From 989edf2b8607dd90a96ec5436277622e32c6a197 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Mon, 21 Jun 2021 13:27:18 -0400 Subject: [PATCH 001/118] [Fleet + Integrations UI] Address UI Regressions in Fleet/Integrations (#102250) (#102759) * Fix active tabs in integrations UI Fixes #101771 * Remove duplicate base breadcrumb Fixes #101785 * Fix i18n --- .../integrations/hooks/use_breadcrumbs.tsx | 9 +-------- .../sections/epm/screens/home/index.tsx | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx index 5c1745be0c9e4..19f72fdc69bba 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx @@ -22,14 +22,7 @@ const BASE_BREADCRUMB: ChromeBreadcrumb = { const breadcrumbGetters: { [key in Page]?: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; } = { - integrations: () => [ - BASE_BREADCRUMB, - { - text: i18n.translate('xpack.fleet.breadcrumbs.integrationsPageTitle', { - defaultMessage: 'Integrations', - }), - }, - ], + integrations: () => [BASE_BREADCRUMB], integrations_all: () => [ BASE_BREADCRUMB, { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 6c635d5d0c9c0..fbd6e07e07bbd 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -22,16 +22,18 @@ import { CategoryFacets } from './category_facets'; export const EPMHomePage: React.FC = memo(() => { return ( - - - + + + - - + + + + - - - + + + ); }); From aa2c8be2237100634c03493b2ab95dfadf937aeb Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 21 Jun 2021 14:13:50 -0400 Subject: [PATCH 002/118] [Ingest Node Pipelines] Remove default value for error_distance param (#102222) (#102763) --- .../__jest__/processors/circle.test.tsx | 14 +++++----- .../processor_form/processors/circle.tsx | 26 +++++++------------ 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx index e29bb2ac6e92e..b8c8f6c58f711 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx @@ -54,9 +54,10 @@ describe('Processor: Circle', () => { // Click submit button with only the type defined await saveNewProcessor(); - // Expect form error as "field" and "shape_type" are required parameters + // Expect form error as "field", "shape_type" and "error_distance" are required parameters expect(form.getErrorsMessages()).toEqual([ 'A field value is required.', + 'An error distance value is required.', 'A shape type value is required.', ]); }); @@ -91,15 +92,15 @@ describe('Processor: Circle', () => { form, } = testBed; - // Add "field" value (required) + // Set required parameters form.setInputValue('fieldNameField.input', 'field_1'); - // Select the shape form.setSelectValue('shapeSelectorField', 'geo_shape'); - // Add "target_field" value - form.setInputValue('targetField.input', 'target_field'); - form.setInputValue('errorDistanceField.input', '10'); + // Set optional parameters + form.setInputValue('targetField.input', 'target_field'); + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + // Save the field with new changes await saveNewProcessor(); @@ -109,6 +110,7 @@ describe('Processor: Circle', () => { error_distance: 10, shape_type: 'geo_shape', target_field: 'target_field', + ignore_missing: true, }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx index 74a7f37d841ae..87e08eaeea6e6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx @@ -13,6 +13,7 @@ import { EuiCode } from '@elastic/eui'; import { FIELD_TYPES, fieldValidators, + fieldFormatters, UseField, SelectField, NumericField, @@ -24,13 +25,13 @@ import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; const { emptyField } = fieldValidators; +const { toInt } = fieldFormatters; const fieldsConfig: FieldsConfig = { /* Required fields config */ error_distance: { type: FIELD_TYPES.NUMBER, - deserializer: (v) => (typeof v === 'number' && !isNaN(v) ? v : 1.0), - serializer: Number, + formatters: [toInt], label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel', { @@ -49,18 +50,11 @@ const fieldsConfig: FieldsConfig = { ), validations: [ { - validator: ({ value }) => { - return isNaN(Number(value)) - ? { - message: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', - { - defaultMessage: 'An error distance value is required.', - } - ), - } - : undefined; - }, + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', { + defaultMessage: 'An error distance value is required.', + }) + ), }, ], }, @@ -110,14 +104,14 @@ export const Circle: FunctionComponent = () => { options: [ { value: 'shape', - label: i18n.translate( + text: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeShape', { defaultMessage: 'Shape' } ), }, { value: 'geo_shape', - label: i18n.translate( + text: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeGeoShape', { defaultMessage: 'Geo-shape' } ), From 9d934af005dd0b8432e05defbcaf4b7bf8ce70ae Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 14:14:49 -0400 Subject: [PATCH 003/118] [Integrations UI] Add updated integrations subtitle (#102738) (#102766) * Add updated integrations subtitle * Fix header image declarations * Use theme hook for dark mode setting Co-authored-by: Kyle Pollich --- .../integrations/layouts/default.tsx | 62 ++++++++++++++++--- .../sections/epm/screens/home/header.tsx | 62 ------------------- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 4 files changed, 55 insertions(+), 73 deletions(-) delete mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx diff --git a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx index 98b8e9515e689..66b88c020f163 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx @@ -5,13 +5,19 @@ * 2.0. */ import React, { memo } from 'react'; -import { EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import styled, { useTheme } from 'styled-components'; + +import type { EuiTheme } from 'src/plugins/kibana_react/common'; + import { useLink } from '../../../hooks'; import type { Section } from '../sections'; -import { HeroImage } from '../sections/epm/screens/home/header'; +import { useLinks } from '../hooks'; import { WithHeaderLayout } from './'; @@ -20,6 +26,30 @@ interface Props { children?: React.ReactNode; } +const Illustration = styled(EuiImage)` + margin-bottom: -68px; + width: 80%; +`; + +const HeroImage = memo(() => { + const { toAssets } = useLinks(); + const theme = useTheme() as EuiTheme; + const IS_DARK_THEME = theme.darkMode; + + return ( + + ); +}); + export const DefaultLayout: React.FunctionComponent = memo(({ section, children }) => { const { getHref } = useLink(); @@ -27,11 +57,29 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch } leftColumn={ - -

- {' '} -

-
+ + +

+ +

+
+ + + + + +

+ +

+
+
+
} tabs={[ { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx deleted file mode 100644 index 55d058a2d7900..0000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx +++ /dev/null @@ -1,62 +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, { memo } from 'react'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { useLinks, useStartServices } from '../../../../hooks'; - -export const HeroCopy = memo(() => { - return ( - - - -

- -

-
-
- - -

- -

-
-
-
- ); -}); - -const Illustration = styled(EuiImage)` - margin-bottom: -68px; - width: 80%; -`; - -export const HeroImage = memo(() => { - const { toAssets } = useLinks(); - const { uiSettings } = useStartServices(); - const IS_DARK_THEME = uiSettings.get('theme:darkMode'); - - return ( - - ); -}); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1e47f0016c48b..0a6019dbd5111 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9070,7 +9070,6 @@ "xpack.fleet.epm.categoryLabel": "カテゴリー", "xpack.fleet.epm.detailsTitle": "詳細", "xpack.fleet.epm.featuresLabel": "機能", - "xpack.fleet.epm.illustrationAltText": "統合の例", "xpack.fleet.epm.licenseLabel": "ライセンス", "xpack.fleet.epm.loadingIntegrationErrorTitle": "統合詳細の読み込みエラー", "xpack.fleet.epm.packageDetails.integrationList.agentCount": "エージェント", @@ -9085,7 +9084,6 @@ "xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "ポリシー", "xpack.fleet.epm.packageDetailsNav.settingsLinkText": "設定", "xpack.fleet.epm.pageSubtitle": "一般的なアプリやサービスの統合を参照する", - "xpack.fleet.epm.pageTitle": "統合", "xpack.fleet.epm.releaseBadge.betaDescription": "この統合は本番環境用ではありません。", "xpack.fleet.epm.releaseBadge.betaLabel": "ベータ", "xpack.fleet.epm.releaseBadge.experimentalDescription": "この統合は、急に変更されたり、将来のリリースで削除されたりする可能性があります。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fb503fa1b1366..afd6b2208c711 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9156,7 +9156,6 @@ "xpack.fleet.epm.categoryLabel": "类别", "xpack.fleet.epm.detailsTitle": "详情", "xpack.fleet.epm.featuresLabel": "功能", - "xpack.fleet.epm.illustrationAltText": "集成的图示", "xpack.fleet.epm.licenseLabel": "许可证", "xpack.fleet.epm.loadingIntegrationErrorTitle": "加载集成详情时出错", "xpack.fleet.epm.packageDetails.integrationList.agentCount": "代理", @@ -9171,7 +9170,6 @@ "xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "策略", "xpack.fleet.epm.packageDetailsNav.settingsLinkText": "设置", "xpack.fleet.epm.pageSubtitle": "浏览集成以了解热门应用和服务。", - "xpack.fleet.epm.pageTitle": "集成", "xpack.fleet.epm.releaseBadge.betaDescription": "在生产环境中不推荐使用此集成。", "xpack.fleet.epm.releaseBadge.betaLabel": "公测版", "xpack.fleet.epm.releaseBadge.experimentalDescription": "此集成可能有重大更改或将在未来版本中移除。", From fc063f1386d9ae1b5e74654db8f7293a07c94156 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 14:17:11 -0400 Subject: [PATCH 004/118] Fixes issue with undo/redo (#101954) (#102765) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Corey Robertson --- .../view_menu/view_menu.component.tsx | 6 - .../workpad/hooks/use_workpad_history.test.ts | 272 ++++++++++++++---- .../workpad/hooks/use_workpad_history.ts | 5 +- 3 files changed, 217 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx index 8f92db4e7f3f4..8fb24c1f3c62e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx @@ -85,10 +85,6 @@ export interface Props { * Current autoplay interval */ autoplayInterval: number; - /** - * Enables autoplay - */ - enableAutoplay: (autoplay: boolean) => void; /** * Sets autoplay interval */ @@ -110,7 +106,6 @@ export const ViewMenu: FunctionComponent = ({ setRefreshInterval, autoplayEnabled, autoplayInterval, - enableAutoplay, setAutoplayInterval, }) => { const setRefresh = (val: number) => setRefreshInterval(val); @@ -259,6 +254,5 @@ ViewMenu.propTypes = { setRefreshInterval: PropTypes.func.isRequired, autoplayEnabled: PropTypes.bool.isRequired, autoplayInterval: PropTypes.number.isRequired, - enableAutoplay: PropTypes.func.isRequired, setAutoplayInterval: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts index b5b9c038cfd2d..515da36ddbb36 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts @@ -26,76 +26,234 @@ describe('useRestoreHistory', () => { jest.resetAllMocks(); }); - test('replaces undefined state with current state', () => { - const history = { - location: { - state: undefined, - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; - - const state = { - persistent: { some: 'state' }, - }; - - mockGetState.mockReturnValue(state); - mockGetHistory.mockReturnValue(history); - - renderHook(() => useWorkpadHistory()); - - expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent)); + describe('initial run', () => { + test('with undefined location state ', () => { + const history = { + location: { + state: undefined, + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent)); + expect(history.push).not.toBeCalled(); + }); + + test('with location state not matching store state', () => { + const history = { + location: { + state: encode({ prior: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); + + test('with location state matching store state', () => { + const state = { some: 'state' }; + const history = { + location: { + state: encode(state), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); }); - test('does not do a push on initial render if states do not match', () => { - const history = { - location: { - state: encode({ old: 'state' }), - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; + describe('state changes', () => { + it('does a replace if location state is undefined', () => { + const push = jest.fn(); + const replace = jest.fn(); + + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + search: '', + }, + push, + replace, + }; + + const state = { + persistent: { some: 'state' }, + }; + + const newState = { + persistent: { new: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + // History object from react router will not change, so just modifying here + history.location.state = undefined; + history.location.pathname = 'newpath'; + rerender(); - const state = { - persistent: { some: 'state' }, - }; + expect(history.replace).toBeCalledWith('newpath', encode(newState.persistent)); + }); - mockGetState.mockReturnValue(state); - mockGetHistory.mockReturnValue(history); + test('does a push if location state does not match store state', () => { + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; - renderHook(() => useWorkpadHistory()); + const oldState = { + persistent: { some: 'state' }, + }; - expect(history.push).not.toBeCalled(); + const newState = { + persistent: { new: 'state' }, + }; + + mockGetState.mockReturnValue(oldState); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + rerender(); + + expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent)); + }); + + test('does nothing if new state matches location state', () => { + const state = { + persistent: { some: 'state' }, + }; + + const newState = { ...state }; + + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + rerender(); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); }); - test('rerender does a push if location state does not match store state', () => { - const history = { - location: { - state: encode({ old: 'state' }), - pathname: 'somepath', - }, - push: jest.fn(), - replace: jest.fn(), - }; + describe('changes to location', () => { + test('changes to pathname have no effect', () => { + // This is equivalent of navigating to a new page. + // The location state will initially be undefined, but + // we don't want to take any action because it will cause a state change + // and that will be picked up and do the replace + const state = { + persistent: { some: 'state' }, + }; + + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + history.location.state = undefined; + history.location.pathname = 'newpath'; + + rerender(); + + expect(history.push).not.toBeCalled(); + expect(history.replace).not.toBeCalled(); + }); - const oldState = { - persistent: { some: 'state' }, - }; + test('changes to search does a replace', () => { + // This is equivalent of going from full screen to not full screen + // There is no state change that will occur, but we still need to update + // the location state + const state = { + persistent: { some: 'state' }, + }; - const newState = { - persistent: { new: 'state' }, - }; + const history = { + location: { + state: encode(state.persistent), + pathname: 'somepath', + search: '', + }, + push: jest.fn(), + replace: jest.fn(), + }; - mockGetState.mockReturnValue(oldState); - mockGetHistory.mockReturnValue(history); + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); - const { rerender } = renderHook(() => useWorkpadHistory()); + const { rerender } = renderHook(() => useWorkpadHistory()); + history.location.pathname = 'somepath'; + history.location.search = 'newsearch'; + history.location.state = undefined; - mockGetState.mockReturnValue(newState); - rerender(); + rerender(); - expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent)); + expect(history.push).not.toBeCalled(); + expect(history.replace).toBeCalledWith( + `somepath?${history.location.search}`, + encode(state.persistent) + ); + }); }); }); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts index 1f563f7147330..b8880be60e36a 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts @@ -31,11 +31,10 @@ export const useWorkpadHistory = () => { // This will happen when navigating directly to a url (there will be no state on that link click) if (locationState === undefined) { history.replace(fullPath, encode(historyState)); - } else if (!doesStateMatchLocationState && !isInitialRun) { + } else if (!isInitialRun && !doesStateMatchLocationState) { // There was a state change here - // If the state of the route that we are on does not match this new state, then we are going to push history.push(fullPath, encode(historyState)); } - }, [history, historyState]); + }, [history, historyState, history.location.search]); }; From 36d94eeeafb4c33a8646dae6a62dfc732cf32413 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 14:34:59 -0400 Subject: [PATCH 005/118] Adding refresh to ensure status is updated (#102742) (#102768) Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> --- .../security_and_spaces/tests/common/comments/post_comment.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index e30e276c7b717..f22ce54de193a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -386,6 +386,8 @@ export default ({ getService }: FtrProviderContext): void => { }, }); + await es.indices.refresh({ index: alert._index }); + const { body: updatedAlert } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') @@ -439,6 +441,8 @@ export default ({ getService }: FtrProviderContext): void => { }, }); + await es.indices.refresh({ index: alert._index }); + const { body: updatedAlert } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) .set('kbn-xsrf', 'true') From 859d3ed2bfdec10cb11a52f1b6e1758a31201c8c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 14:37:30 -0400 Subject: [PATCH 006/118] [Alerting][Docs] Removing placeholder sections in docs (#101953) (#102790) * Removing placeholder prerequisite section * Removing placeholder defining-rules and rule-management * Fixing links * Setup to set up Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: ymao1 --- docs/user/alerting/alerting-getting-started.asciidoc | 5 ----- docs/user/alerting/alerting-setup.asciidoc | 4 ++-- docs/user/alerting/defining-rules.asciidoc | 11 ----------- docs/user/alerting/index.asciidoc | 2 -- docs/user/alerting/rule-management.asciidoc | 5 ----- docs/user/alerting/rule-types.asciidoc | 2 +- 6 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 docs/user/alerting/defining-rules.asciidoc delete mode 100644 docs/user/alerting/rule-management.asciidoc diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 8c17f8ec93b96..b699c56ebd944 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -136,9 +136,4 @@ Functionally, {kib} alerting differs in that: At a higher level, {kib} alerting allows rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *rule types* simplify setup and hide the details of complex, domain-specific detections, while providing a consistent interface across {kib}. -[float] -[[alerting-setup-prerequisites]] -== Prerequisites -<> - -- \ No newline at end of file diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 39f1af0030e0a..2ae5160069f0a 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[alerting-setup]] -== Alerting Setup +== Alerting Set up ++++ -Setup +Set up ++++ The Alerting feature is automatically enabled in {kib}, but might require some additional configuration. diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc deleted file mode 100644 index 686a7bbc8a37b..0000000000000 --- a/docs/user/alerting/defining-rules.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[role="xpack"] -[[defining-alerts]] -== Defining rules - -This content has been moved to <>. - -[float] -[[defining-alerts-general-details]] -==== General rule details - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 9ab6a2dc46ebf..957d99a54ebaa 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,7 +1,5 @@ include::alerting-getting-started.asciidoc[] include::alerting-setup.asciidoc[] include::create-and-manage-rules.asciidoc[] -include::defining-rules.asciidoc[] -include::rule-management.asciidoc[] include::rule-types.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc deleted file mode 100644 index d6349a60e08eb..0000000000000 --- a/docs/user/alerting/rule-management.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[role="xpack"] -[[alert-management]] -== Managing rules - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc index bb840014fe80f..f7f57d2f845a0 100644 --- a/docs/user/alerting/rule-types.asciidoc +++ b/docs/user/alerting/rule-types.asciidoc @@ -15,7 +15,7 @@ see {subscriptions}[the subscription page]. [[stack-rules]] === Stack rules -<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. +<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. [cols="2*<"] |=== From ed5e1842d3f7e555f2bb7ad5d3c44888cc9b6db1 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 14:40:05 -0400 Subject: [PATCH 007/118] [App Search] Convert Engines overview & engine creation to new page template (#102677) (#102769) * Convert Engine Creation page to new page template + simplify EuiForm/form to 1 level of nesting * Convert Meta Engine Creation page to new page template * Initial conversion of Engines overview to new page template * Engines Overview: simplify loading & empty state to use new page template props * Engines Overview: simplify header - now that the header/actions, chrome/telemetry, & empty state etc. are all DRY'd out by the template, there's no need for a separate header component - we can simply dry out the header action, which will shortly be removed with the 7.14 launch - note: for empty state changes: mostly indenting, the new template automatically creates a subdued panel for us in empty state so no need to include one manually * Fix broken test mocks I definitely should not have been importing the index file I'm mocking, but why did this only break now?? :psyduck: Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance --- .../app_search/__mocks__/engine_logic.mock.ts | 2 +- .../engine_creation/engine_creation.tsx | 146 ++++++++------- .../engines/components/empty_state.tsx | 136 ++++++-------- .../components/engines/components/header.tsx | 54 ------ .../components/engines/components/index.ts | 3 +- ...der.test.tsx => launch_as_button.test.tsx} | 18 +- .../engines/components/launch_as_button.tsx | 41 +++++ .../engines/components/loading_state.test.tsx | 22 --- .../engines/components/loading_state.tsx | 25 --- .../components/engines/constants.ts | 5 + .../engines/engines_overview.test.tsx | 173 ++++++++---------- .../components/engines/engines_overview.tsx | 38 ++-- .../meta_engine_creation.tsx | 29 ++- .../public/applications/app_search/index.tsx | 32 ++-- 14 files changed, 309 insertions(+), 415 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/{header.test.tsx => launch_as_button.test.tsx} (64%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts index 23d638d5f25f3..b38659b7a9a79 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts @@ -6,7 +6,7 @@ */ import { EngineDetails } from '../components/engine/types'; -import { ENGINES_TITLE } from '../components/engines'; +import { ENGINES_TITLE } from '../components/engines/constants'; import { generateEncodedPath } from '../utils/encode_path_params'; export const mockEngineValues = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx index 250c941009ecb..913aa4f0ec845 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx @@ -16,15 +16,13 @@ import { EuiFlexItem, EuiFieldText, EuiSelect, - EuiPageHeader, - EuiPageContent, + EuiPanel, EuiSpacer, EuiTitle, EuiButton, } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { ALLOWED_CHARS_NOTE, @@ -44,77 +42,77 @@ export const EngineCreation: React.FC = () => { const { setLanguage, setRawName, submitEngine } = useActions(EngineCreationLogic); return ( -
- - - - - -
{ - e.preventDefault(); - submitEngine(); - }} - > - -

{ENGINE_CREATION_FORM_TITLE}

-
- - - - 0 && rawName !== name ? ( - <> - {SANITIZED_NAME_NOTE} {name} - - ) : ( - ALLOWED_CHARS_NOTE - ) - } + + + { + e.preventDefault(); + submitEngine(); + }} + > + +

{ENGINE_CREATION_FORM_TITLE}

+
+ + + + 0 && rawName !== name ? ( + <> + {SANITIZED_NAME_NOTE} {name} + + ) : ( + ALLOWED_CHARS_NOTE + ) + } + fullWidth + > + setRawName(event.currentTarget.value)} + autoComplete="off" fullWidth - > - setRawName(event.currentTarget.value)} - autoComplete="off" - fullWidth - data-test-subj="EngineCreationNameInput" - placeholder={ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER} - autoFocus - /> - - - - - setLanguage(event.currentTarget.value)} - /> - - - - - - {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} - - + data-test-subj="EngineCreationNameInput" + placeholder={ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER} + autoFocus + /> +
+
+ + + setLanguage(event.currentTarget.value)} + /> + + +
+ + + {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} +
-
-
+ + ); }; 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 e6a7c03d2aab4..df17d22d387d9 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 @@ -9,7 +9,7 @@ import React from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageContent, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; @@ -20,86 +20,72 @@ import { ENGINE_CREATION_PATH } from '../../../routes'; import { SampleEngineCreationCta } from '../../sample_engine_creation_cta/sample_engine_creation_cta'; -import { EnginesOverviewHeader } from './header'; - export const EmptyState: React.FC = () => { const { myRole: { canManageEngines }, } = useValues(AppLogic); const { sendAppSearchTelemetry } = useActions(TelemetryLogic); - return ( - <> - - - {canManageEngines ? ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', { - defaultMessage: 'Create your first engine', - })} - - } - titleSize="l" - body={ -

- {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.description1', { - defaultMessage: - 'An App Search engine stores the documents for your search experience.', - })} -

- } - actions={ - <> - - sendAppSearchTelemetry({ - action: 'clicked', - metric: 'create_first_engine_button', - }) - } - > - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta', - { defaultMessage: 'Create an engine' } - )} - - - - - } - /> - ) : ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', { - defaultMessage: 'No engines available', - })} - - } - body={ -

- {i18n.translate( - 'xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.description', - { - defaultMessage: - 'Contact your App Search administrator to either create or grant you access to an engine.', - } - )} -

+ return canManageEngines ? ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', { + defaultMessage: 'Create your first engine', + })} + + } + titleSize="l" + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.description1', { + defaultMessage: 'An App Search engine stores the documents for your search experience.', + })} +

+ } + actions={ + <> + + sendAppSearchTelemetry({ + action: 'clicked', + metric: 'create_first_engine_button', + }) } - /> - )} -
- + > + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta', { + defaultMessage: 'Create an engine', + })} + + + + + } + /> + ) : ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', { + defaultMessage: 'No engines available', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.description', { + defaultMessage: + 'Contact your App Search administrator to either create or grant you access to an engine.', + })} +

+ } + /> ); }; 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 deleted file mode 100644 index bab67fd0e4bb5..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ /dev/null @@ -1,54 +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 { useActions } from 'kea'; - -import { EuiPageHeader, EuiButton } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { TelemetryLogic } from '../../../../shared/telemetry'; - -import { ENGINES_TITLE } from '../constants'; - -export const EnginesOverviewHeader: React.FC = () => { - const { sendAppSearchTelemetry } = useActions(TelemetryLogic); - - return ( - <> - - - sendAppSearchTelemetry({ - action: 'clicked', - metric: 'header_launch_button', - }) - } - data-test-subj="launchButton" - > - {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', { - defaultMessage: 'Launch App Search', - })} - , - ]} - /> - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts index 234d3ba31f44b..1d8e578e0edf2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { EnginesOverviewHeader } from './header'; -export { LoadingState } from './loading_state'; +export { LaunchAppSearchButton } from './launch_as_button'; export { EmptyState } from './empty_state'; export { EmptyMetaEnginesState } from './empty_meta_engines_state'; 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/launch_as_button.test.tsx similarity index 64% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx index 9b245a468b083..93c91cc3830f4 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/launch_as_button.test.tsx @@ -12,23 +12,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - -import { EnginesOverviewHeader } from './'; - -describe('EnginesOverviewHeader', () => { - const wrapper = shallow() - .find(EuiPageHeader) - .dive() - .children() - .dive(); - - it('renders', () => { - expect(wrapper.find('h1').text()).toEqual('Engines overview'); - }); +import { LaunchAppSearchButton } from './'; +describe('LaunchAppSearchButton', () => { it('renders a launch app search button that sends telemetry on click', () => { - const button = wrapper.find('[data-test-subj="launchButton"]'); + const button = shallow(); expect(button.prop('href')).toBe('http://localhost:3002/as'); expect(button.prop('isDisabled')).toBeFalsy(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx new file mode 100644 index 0000000000000..41102cb4fba2e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx @@ -0,0 +1,41 @@ +/* + * 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 { useActions } from 'kea'; + +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../../shared/telemetry'; + +export const LaunchAppSearchButton: React.FC = () => { + const { sendAppSearchTelemetry } = useActions(TelemetryLogic); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + sendAppSearchTelemetry({ + action: 'clicked', + metric: 'header_launch_button', + }) + } + data-test-subj="launchButton" + > + {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', { + defaultMessage: 'Launch App Search', + })} + + ); +}; 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 deleted file mode 100644 index f7ccfea4bb4d4..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx +++ /dev/null @@ -1,22 +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 { shallow } from 'enzyme'; - -import { EuiLoadingContent } from '@elastic/eui'; - -import { LoadingState } from './'; - -describe('LoadingState', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiLoadingContent)).toHaveLength(2); - }); -}); 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 deleted file mode 100644 index 875c47378d1fb..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx +++ /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 React from 'react'; - -import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; - -import { EnginesOverviewHeader } from './header'; - -export const LoadingState: React.FC = () => { - return ( - <> - - - - - - - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts index 8d03e3d23ae23..d01e89e004d28 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts @@ -11,6 +11,11 @@ export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.en defaultMessage: 'Engines', }); +export const ENGINES_OVERVIEW_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.title', + { defaultMessage: 'Engines overview' } +); + export const META_ENGINES_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.metaEngines.title', { defaultMessage: 'Meta Engines' } 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 27fe65fe518eb..8825c322fb8d5 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 @@ -14,7 +14,6 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { rerender } from '../../../test_helpers'; -import { LoadingState, EmptyState } from './components'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; @@ -61,135 +60,117 @@ describe('EnginesOverview', () => { setMockActions(actions); }); - describe('non-happy-path states', () => { - it('isLoading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); + const valuesWithEngines = { + ...values, + dataLoading: false, + engines: ['test-engine'], + enginesMeta: { + page: { + current: 1, + size: 10, + total_results: 100, + }, + }, + }; - expect(wrapper.find(LoadingState)).toHaveLength(1); - }); + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(valuesWithEngines); + }); - it('isEmpty', () => { - setMockValues({ ...values, engines: [] }); - const wrapper = shallow(); + it('renders and calls the engines API', () => { + const wrapper = shallow(); - expect(wrapper.find(EmptyState)).toHaveLength(1); - }); + expect(wrapper.find(EnginesTable)).toHaveLength(1); + expect(actions.loadEngines).toHaveBeenCalled(); }); - describe('happy-path states', () => { - const valuesWithEngines = { - ...values, - dataLoading: false, - engines: ['test-engine'], - enginesMeta: { - page: { - current: 1, - size: 10, - total_results: 100, - }, - }, - }; + describe('when the user can manage/create engines', () => { + it('renders a create engine button which takes users to the create engine page', () => { + setMockValues({ + ...valuesWithEngines, + myRole: { canManageEngines: true }, + }); + const wrapper = shallow(); - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(valuesWithEngines); + expect( + wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to') + ).toEqual('/engine_creation'); }); + }); - it('renders and calls the engines API', () => { + describe('when the account has a platinum license', () => { + it('renders a 2nd meta engines table & makes a 2nd meta engines call', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: true, + }); const wrapper = shallow(); - expect(wrapper.find(EnginesTable)).toHaveLength(1); - expect(actions.loadEngines).toHaveBeenCalled(); + expect(wrapper.find(MetaEnginesTable)).toHaveLength(1); + expect(actions.loadMetaEngines).toHaveBeenCalled(); }); describe('when the user can manage/create engines', () => { - it('renders a create engine button which takes users to the create engine page', () => { + it('renders a create engine button which takes users to the create meta engine page', () => { setMockValues({ ...valuesWithEngines, + hasPlatinumLicense: true, myRole: { canManageEngines: true }, }); const wrapper = shallow(); expect( - wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to') - ).toEqual('/engine_creation'); + wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to') + ).toEqual('/meta_engine_creation'); }); }); + }); - describe('when the account has a platinum license', () => { - it('renders a 2nd meta engines table & makes a 2nd meta engines call', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: true, - }); - const wrapper = shallow(); + describe('pagination', () => { + const getTablePagination = (wrapper: ShallowWrapper) => + wrapper.find(EnginesTable).prop('pagination'); - expect(wrapper.find(MetaEnginesTable)).toHaveLength(1); - expect(actions.loadMetaEngines).toHaveBeenCalled(); - }); + it('passes down page data from the API', () => { + const wrapper = shallow(); + const pagination = getTablePagination(wrapper); - describe('when the user can manage/create engines', () => { - it('renders a create engine button which takes users to the create meta engine page', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: true, - myRole: { canManageEngines: true }, - }); - const wrapper = shallow(); - - expect( - wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to') - ).toEqual('/meta_engine_creation'); - }); - }); + expect(pagination.totalItemCount).toEqual(100); + expect(pagination.pageIndex).toEqual(0); }); - describe('pagination', () => { - const getTablePagination = (wrapper: ShallowWrapper) => - wrapper.find(EnginesTable).prop('pagination'); - - it('passes down page data from the API', () => { - const wrapper = shallow(); - const pagination = getTablePagination(wrapper); + it('re-polls the API on page change', () => { + const wrapper = shallow(); - expect(pagination.totalItemCount).toEqual(100); - expect(pagination.pageIndex).toEqual(0); + setMockValues({ + ...valuesWithEngines, + enginesMeta: { + page: { + ...valuesWithEngines.enginesMeta.page, + current: 51, + }, + }, }); + rerender(wrapper); - it('re-polls the API on page change', () => { - const wrapper = shallow(); - - setMockValues({ - ...valuesWithEngines, - enginesMeta: { - page: { - ...valuesWithEngines.enginesMeta.page, - current: 51, - }, - }, - }); - rerender(wrapper); + expect(actions.loadEngines).toHaveBeenCalledTimes(2); + expect(getTablePagination(wrapper).pageIndex).toEqual(50); + }); - expect(actions.loadEngines).toHaveBeenCalledTimes(2); - expect(getTablePagination(wrapper).pageIndex).toEqual(50); + it('calls onPagination handlers', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: true, + metaEngines: ['test-meta-engine'], }); + const wrapper = shallow(); + const pageEvent = { page: { index: 0 } }; - it('calls onPagination handlers', () => { - setMockValues({ - ...valuesWithEngines, - hasPlatinumLicense: true, - metaEngines: ['test-meta-engine'], - }); - const wrapper = shallow(); - const pageEvent = { page: { index: 0 } }; - - wrapper.find(EnginesTable).simulate('change', pageEvent); - expect(actions.onEnginesPagination).toHaveBeenCalledWith(1); + wrapper.find(EnginesTable).simulate('change', pageEvent); + expect(actions.onEnginesPagination).toHaveBeenCalledWith(1); - wrapper.find(MetaEnginesTable).simulate('change', pageEvent); - expect(actions.onMetaEnginesPagination).toHaveBeenCalledWith(1); - }); + wrapper.find(MetaEnginesTable).simulate('change', pageEvent); + expect(actions.onMetaEnginesPagination).toHaveBeenCalledWith(1); }); }); }); 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 7001ecada999a..44111a5ecbe66 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 @@ -12,7 +12,7 @@ import { useValues, useActions } from 'kea'; import { EuiFlexGroup, EuiFlexItem, - EuiPageContent, + EuiPanel, EuiPageContentHeader, EuiPageContentHeaderSection, EuiPageContentBody, @@ -20,24 +20,19 @@ import { EuiSpacer, } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; import { LicensingLogic } from '../../../shared/licensing'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination'; -import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; import { EngineIcon, MetaEngineIcon } from '../../icons'; import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes'; +import { AppSearchPageTemplate } from '../layout'; -import { - EnginesOverviewHeader, - LoadingState, - EmptyState, - EmptyMetaEnginesState, -} from './components'; +import { LaunchAppSearchButton, EmptyState, EmptyMetaEnginesState } from './components'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; import { + ENGINES_OVERVIEW_TITLE, CREATE_AN_ENGINE_BUTTON_LABEL, CREATE_A_META_ENGINE_BUTTON_LABEL, ENGINES_TITLE, @@ -73,16 +68,19 @@ export const EnginesOverview: React.FC = () => { if (hasPlatinumLicense) loadMetaEngines(); }, [hasPlatinumLicense, metaEnginesMeta.page.current]); - if (dataLoading) return ; - if (!engines.length) return ; - return ( - <> - - - - - + ], + }} + isLoading={dataLoading} + isEmptyState={!engines.length} + emptyState={} + > + @@ -168,7 +166,7 @@ export const EnginesOverview: React.FC = () => { )} - - + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx index 02a1768a7528e..325e557acec0c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx @@ -18,16 +18,14 @@ import { EuiFormRow, EuiFlexItem, EuiFieldText, - EuiPageContent, - EuiPageHeader, + EuiPanel, EuiSpacer, EuiTitle, EuiButton, } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { AppLogic } from '../../app_logic'; +import { AppSearchPageTemplate } from '../layout'; import { ALLOWED_CHARS_NOTE, @@ -74,20 +72,21 @@ export const MetaEngineCreation: React.FC = () => { }, []); return ( -
- - {META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION}
{META_ENGINE_CREATION_FORM_DOCUMENTATION_DESCRIPTION} - } - /> - - + ), + }} + data-test-subj="MetaEngineCreation" + > + { {META_ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} - -
+ + ); }; 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 caf0f805e8ca7..b2cd3d7b54a1a 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 @@ -92,6 +92,22 @@ export const AppSearchConfigured: React.FC> = (props) = )} + + + + + + + {canManageEngines && ( + + + + )} + {canManageMetaEngines && ( + + + + )} {canViewRoleMappings && ( @@ -100,12 +116,6 @@ export const AppSearchConfigured: React.FC> = (props) = } readOnlyMode={readOnlyMode}> - - - - - - @@ -115,16 +125,6 @@ export const AppSearchConfigured: React.FC> = (props) = - {canManageEngines && ( - - - - )} - {canManageMetaEngines && ( - - - - )} From 4ce951f2d10f60832bbe597ac1a8ea2e7147fc74 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 21 Jun 2021 21:12:16 +0200 Subject: [PATCH 008/118] [7.x] Cleanup outdated @elastic/elasticsearch client type errors (#101741) (#102747) * Cleanup outdated @elastic/elasticsearch client type errors (#101741) * fix errors and update comments in Core * fix errors or update comments in Security plugin * update spaces test * update task_manager files * update comments in monitoring plugin * fix errors in update comments in security_solutions * fix errors and update comments in data_enhanced * update fleet code * update infra code * update comment in trigger_actions_ui * update comment in lens * update comments in ES-UI code * update typings for search * update monitoring * remove outdated export # Conflicts: # x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts # x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts * fix a result of wrong conflict resolution --- .../core_usage_data/core_usage_data_service.ts | 14 ++++++-------- .../migrations/core/index_migrator.ts | 8 ++------ .../saved_objects/service/lib/repository.ts | 4 ++-- .../get_saved_object_counts.ts | 2 +- .../server/check_cluster_data.test.ts | 18 +++++++----------- typings/elasticsearch/search.d.ts | 2 ++ .../data_enhanced/server/collectors/fetch.ts | 10 +++++++--- .../server/search/session/get_search_status.ts | 2 +- .../server/routes/data_streams/handlers.ts | 12 +++++++----- .../services/api_keys/enrollment_api_key.ts | 2 +- .../evaluate_condition.ts | 4 ++-- .../log_entries/log_entry_search_strategy.ts | 2 +- .../lens/server/usage/visualization_counts.ts | 2 +- .../lib/alerts/fetch_ccr_read_exceptions.ts | 2 +- .../alerts/fetch_cpu_usage_node_stats.test.ts | 6 +++--- .../alerts/fetch_disk_usage_node_stats.test.ts | 2 +- .../lib/alerts/fetch_disk_usage_node_stats.ts | 2 +- .../lib/alerts/fetch_index_shard_size.ts | 2 +- .../lib/alerts/fetch_kibana_versions.test.ts | 2 +- .../lib/alerts/fetch_logstash_versions.test.ts | 2 +- .../alerts/fetch_memory_usage_node_stats.ts | 2 +- .../fetch_missing_monitoring_data.test.ts | 4 ++-- .../alerts/fetch_nodes_from_cluster_stats.ts | 2 +- .../fetch_thread_pool_rejections_stats.ts | 2 +- x-pack/plugins/security/common/model/user.ts | 4 ++-- .../management/users/edit_user/user_form.tsx | 4 ++-- .../server/authentication/api_keys/api_keys.ts | 2 +- .../server/authentication/providers/base.ts | 2 +- .../server/authentication/providers/token.ts | 2 +- .../security/server/authentication/tokens.ts | 2 +- .../server/routes/authorization/roles/get.ts | 2 +- .../routes/authorization/roles/get_all.ts | 2 +- .../security/server/routes/role_mapping/get.ts | 2 +- .../routes/resolver/tree/queries/stats.ts | 2 +- .../migrations/get_signal_versions_by_index.ts | 5 ++--- .../migrations/get_signals_indices_in_range.ts | 5 ++--- .../threshold/bulk_create_threshold_signals.ts | 1 - .../factory/hosts/overview/index.ts | 2 +- .../factory/network/overview/index.ts | 2 +- .../server/routes/api/snapshots.ts | 1 - .../monitoring/workload_statistics.test.ts | 10 +++++----- .../plugins/task_manager/server/task_store.ts | 10 ++++------ .../transform/server/routes/api/transforms.ts | 2 +- .../server/data/lib/time_series_query.ts | 2 +- .../common/suites/copy_to_space.ts | 9 ++++++--- 45 files changed, 89 insertions(+), 94 deletions(-) diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index dc24f889cd8dd..afe1b45175f86 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -126,14 +126,12 @@ export class CoreUsageDataService implements CoreService): SavedObjectsFindResult => ({ - // @ts-expect-error @elastic/elasticsearch declared Id as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional ...this._rawToSavedObject(hit), score: hit._score!, - // @ts-expect-error @elastic/elasticsearch declared sort as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional sort: hit.sort, }) ), diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts index 9927b27da6c8f..eeaeed67e753f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts @@ -26,6 +26,6 @@ export async function getSavedObjectsCounts( }, }; const { body } = await esClient.search(savedObjectCountSearchParams); - // @ts-expect-error @elastic/elasticsearch Aggregate does not include `buckets` + // @ts-expect-error declare type for aggregations explicitly return body.aggregations?.types?.buckets || []; } diff --git a/src/plugins/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts index 9e9459a68754c..6aa1cc9a28c39 100644 --- a/src/plugins/security_oss/server/check_cluster_data.test.ts +++ b/src/plugins/security_oss/server/check_cluster_data.test.ts @@ -27,20 +27,19 @@ describe('checkClusterForUserData', () => { it('returns false if data only exists in system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'kibana_sample_ecommerce_data', - 'docs.count': 20, + 'docs.count': '20', }, { index: '.somethingElse', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -56,16 +55,15 @@ describe('checkClusterForUserData', () => { it('returns true if data exists in non-system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -87,23 +85,21 @@ describe('checkClusterForUserData', () => { ) .mockRejectedValueOnce(new Error('something terrible happened')) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, ], }) ) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) diff --git a/typings/elasticsearch/search.d.ts b/typings/elasticsearch/search.d.ts index 36a684fb097a5..0960fb189a341 100644 --- a/typings/elasticsearch/search.d.ts +++ b/typings/elasticsearch/search.d.ts @@ -417,7 +417,9 @@ export type AggregateOf< { key: string; from?: number; + from_as_string?: string; to?: number; + to_as_string?: string; doc_count: number; }, TAggregationContainer extends { range: { ranges: Array } } diff --git a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts index 6feb13432d07b..bfd5ee745cd75 100644 --- a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts +++ b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; @@ -36,8 +36,12 @@ export function fetchProvider(config$: Observable, logger: L }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregations - const buckets: SessionPersistedTermsBucket[] = esResponse.aggregations!.persisted.buckets; + const aggs = esResponse.aggregations as Record< + string, + estypes.AggregationsMultiBucketAggregate + >; + + const buckets = aggs.persisted.buckets; if (!buckets.length) { return { transientCount: 0, persistedCount: 0, totalCount: 0 }; } diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts index e228ba725489c..461c41b46491c 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts @@ -18,7 +18,7 @@ export async function getSearchStatus( ): Promise> { // TODO: Handle strategies other than the default one try { - // @ts-expect-error @elastic/elasticsearch status method is not defined + // @ts-expect-error start_time_in_millis: EpochMillis is string | number const apiResponse: ApiResponse = await client.asyncSearch.status({ id: asyncId, }); diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index b80ddfe8e7c9b..073ff7806d9fe 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import { keyBy, keys, merge } from 'lodash'; import type { RequestHandler, SavedObjectsBulkGetObject } from 'src/core/server'; @@ -140,10 +140,7 @@ export const getListHandler: RequestHandler = async (context, request, response) // Query backing indices to extract data stream dataset, namespace, and type values const { - body: { - // @ts-expect-error @elastic/elasticsearch aggregations are not typed - aggregations: { dataset, namespace, type }, - }, + body: { aggregations: dataStreamAggs }, } = await esClient.search({ index: dataStream.indices.map((index) => index.index_name), body: { @@ -187,6 +184,11 @@ export const getListHandler: RequestHandler = async (context, request, response) }, }); + const { dataset, namespace, type } = dataStreamAggs as Record< + string, + estypes.AggregationsMultiBucketAggregate<{ key?: string }> + >; + // Set values from backing indices query dataStreamResponse.dataset = dataset.buckets[0]?.key || ''; dataStreamResponse.namespace = namespace.buckets[0]?.key || ''; diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index f9aab997f063c..ba34dbba108d7 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -47,7 +47,7 @@ export async function listEnrollmentApiKeys( body: query ? { query } : undefined, }); - // @ts-expect-error @elastic/elasticsearch + // @ts-expect-error @elastic/elasticsearch _source is optional const items = res.body.hits.hits.map(esDocToEnrollmentApiKey); return { diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 0d5bc9b742fdc..922b10e8bd2b0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -123,8 +123,8 @@ const getData = async ( const client = async ( options: CallWithRequestParams ): Promise> => - // @ts-expect-error @elastic/elasticsearch SearchResponse.body.timeout is not required - (await esClient.search(options as any)).body as InfraDatabaseSearchResponse; + // @ts-expect-error SearchResponse.body.timeout is optional + (await esClient.search(options)).body as InfraDatabaseSearchResponse; const metrics = [ metric === 'custom' ? (customMetric as SnapshotCustomMetricInput) : { type: metric }, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts index aa34204b9fb44..1f0f13eeb6ca9 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts @@ -84,7 +84,7 @@ export const logEntrySearchStrategyProvider = ({ tiebreakerField, runtimeMappings, }): IEsSearchRequest => ({ - // @ts-expect-error @elastic/elasticsearch declares indices_boost as Record + // @ts-expect-error `Field` is not assignable to `SearchRequest.docvalue_fields` params: createGetLogEntryQuery( indices, params.logEntryId, diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts index f0c48fb1152e8..6e79d5f342377 100644 --- a/x-pack/plugins/lens/server/usage/visualization_counts.ts +++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts @@ -75,7 +75,7 @@ export async function getVisualizationCounts( }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response + // @ts-expect-error specify aggregations type explicitly const buckets = results.aggregations.groups.buckets; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index 213e73a4b9534..a96a7454ea744 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -95,7 +95,7 @@ export async function fetchCCRReadExceptions( const { body: response } = await esClient.search(params); const stats: CCRReadExceptionsStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare aggegations type explicitly const { buckets: remoteClusterBuckets = [] } = response.aggregations?.remote_clusters; if (!remoteClusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 0fb9dd5298e9e..9cb773c81923b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -25,7 +25,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch normal stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -79,7 +79,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch container stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -146,7 +146,7 @@ describe('fetchCpuUsageNodeStats', () => { it('fetch properly return ccs', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts index 8faf79fc4b59c..4766400891af5 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts @@ -25,7 +25,7 @@ describe('fetchDiskUsageNodeStats', () => { it('fetch normal stats', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index a51dccd727966..2e8b5c7478e15 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -101,7 +101,7 @@ export async function fetchDiskUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index aab3f0101ef83..117894c0d823b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -105,7 +105,7 @@ export async function fetchIndexShardSize( }; const { body: response } = await esClient.search(params); - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare aggegations type explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; const stats: IndexShardSizeStats[] = []; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts index 2b966b16f2f5c..f9a03bb73d5fc 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -23,7 +23,7 @@ describe('fetchKibanaVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts index d7d4e6531f58e..5732fc00f009b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -23,7 +23,7 @@ describe('fetchLogstashVersions', () => { it('fetch as expected', async () => { esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index 245838541d435..46bb9c794a6a6 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -94,7 +94,7 @@ export async function fetchMemoryUsageNodeStats( const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts index c8d15acf8ff73..980adb009ff8f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts @@ -56,7 +56,7 @@ describe('fetchMissingMonitoringData', () => { ]; esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { @@ -115,7 +115,7 @@ describe('fetchMissingMonitoringData', () => { }, ]; esClient.search.mockReturnValue( - // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values + // @ts-expect-error not full response interface elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index d1a343b9b3eef..5f867ca5b6edf 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -90,7 +90,7 @@ export async function fetchNodesFromClusterStats( const { body: response } = await esClient.search(params); const nodes: AlertClusterStatsNodes[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets + // @ts-expect-error declare type for aggregations explicitly const clusterBuckets = response.aggregations?.clusters?.buckets; if (!clusterBuckets?.length) { return nodes; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index db5943ca67031..954ec3877144f 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -96,7 +96,7 @@ export async function fetchThreadPoolRejectionStats( const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error declare type for aggregations explicitly const { buckets: clusterBuckets } = response.aggregations?.clusters; if (!clusterBuckets?.length) { diff --git a/x-pack/plugins/security/common/model/user.ts b/x-pack/plugins/security/common/model/user.ts index 3f4787b207f88..2bcea659699cb 100644 --- a/x-pack/plugins/security/common/model/user.ts +++ b/x-pack/plugins/security/common/model/user.ts @@ -7,8 +7,8 @@ export interface User { username: string; - email: string; - full_name: string; + email?: string; + full_name?: string; roles: readonly string[]; enabled: boolean; metadata?: { diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx index 29d87e31797cc..8101c09d64907 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -41,8 +41,8 @@ export const THROTTLE_USERS_WAIT = 10000; export interface UserFormValues { username?: string; - full_name: string; - email: string; + full_name?: string; + email?: string; password?: string; confirm_password?: string; roles: readonly string[]; diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index 026de4a978428..639a9f2ec41c9 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -220,7 +220,7 @@ export class APIKeys { try { result = ( await this.clusterClient.asInternalUser.security.grantApiKey({ - // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors + // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors doesn't support `Record` body: params, }) ).body; diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index 99309f182f063..d5b173fcfad8c 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -118,7 +118,7 @@ export abstract class BaseAuthenticationProvider { */ protected async getUser(request: KibanaRequest, authHeaders: Headers = {}) { return this.authenticationInfoToAuthenticatedUser( - // @ts-expect-error @elastic/elasticsearch `AuthenticateResponse` type doesn't define `authentication_type` and `enabled`. + // @ts-expect-error Metadata is defined as Record ( await this.options.client .asScoped({ headers: { ...request.headers, ...authHeaders } }) diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 43338a8f6400f..fae0d7ca69038 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -84,7 +84,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Get token API request to Elasticsearch successful'); return AuthenticationResult.succeeded( this.authenticationInfoToAuthenticatedUser( - // @ts-expect-error @elastic/elasticsearch GetUserAccessTokenResponse declares authentication: string, but expected AuthenticatedUser + // @ts-expect-error @elastic/elasticsearch metadata defined as Record; authenticationInfo as AuthenticationInfo ), { diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index 1adbb2dc66533..47051cc08da23 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -73,7 +73,7 @@ export class Tokens { return { accessToken, refreshToken, - // @ts-expect-error @elastic/elasticsearch declared GetUserAccessTokenResponse.authentication: string + // @ts-expect-error @elastic/elasticsearch user metadata defined as Record authenticationInfo: authenticationInfo as AuthenticationInfo, }; } catch (err) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 01d32f7fb8233..075a8d133f1e6 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -32,7 +32,7 @@ export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { if (elasticsearchRole) { return response.ok({ body: transformElasticsearchRoleToRole( - // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + // @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]` elasticsearchRole, request.params.name, authz.applicationName diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 4d458be4e332f..be0880a06d59d 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -27,7 +27,7 @@ export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams body: Object.entries(elasticsearchRoles) .map(([roleName, elasticsearchRole]) => transformElasticsearchRoleToRole( - // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[] elasticsearchRole, roleName, authz.applicationName diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 67cd8975b76eb..257b4210b13f7 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -36,7 +36,7 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { return { name, ...mapping, - // @ts-expect-error @elastic/elasticsearch `XPackRoleMapping` type doesn't define `role_templates` property. + // @ts-expect-error @elastic/elasticsearch `SecurityRoleMapping` doeesn't contain `role_templates` role_templates: (mapping.role_templates || []).map((entry: RoleTemplate) => { return { ...entry, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index f21259980d464..b64390f4e382f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -128,7 +128,7 @@ export class StatsQuery { index: this.indexPatterns, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + // @ts-expect-error declare aggegations type explicitly return response.body.aggregations?.ids?.buckets.reduce( (cummulative: Record, bucket: CategoriesAgg) => ({ ...cummulative, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts index 784164e430ff0..decde16d77a38 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts @@ -72,9 +72,8 @@ export const getSignalVersionsByIndex = async ({ }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response - const body = response.body as SignalVersionsAggResponse; - const indexBuckets = body.aggregations.signals_indices.buckets; + const aggs = response.body.aggregations as SignalVersionsAggResponse['aggregations']; + const indexBuckets = aggs.signals_indices.buckets; return index.reduce((agg, _index) => { const bucket = indexBuckets.find((ib) => ib.key === _index); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts index 3c9132fc81279..af236b10d0795 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts @@ -72,7 +72,6 @@ export const getSignalsIndicesInRange = async ({ }, }); - // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response - const body = response.body as IndexesResponse; - return body.aggregations.indexes.buckets.map((bucket) => bucket.key); + const aggs = response.body.aggregations as IndexesResponse['aggregations']; + return aggs.indexes.buckets.map((bucket) => bucket.key); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index 08fa2f14a0fd5..f56ed3a5e9eb4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { get } from 'lodash/fp'; import set from 'set-value'; import { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts index c58e450806849..5866695ab1641 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts @@ -24,7 +24,7 @@ export const hostOverview: SecuritySolutionFactory = { options: HostOverviewRequestOptions, response: IEsSearchResponse ): Promise => { - // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations + // @ts-expect-error specify aggregations type explicitly const aggregations: OverviewHostHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewHostQuery(options))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts index 1f85a119f3c8e..069125c6700eb 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts @@ -24,7 +24,7 @@ export const networkOverview: SecuritySolutionFactory = options: NetworkOverviewRequestOptions, response: IEsSearchResponse ): Promise => { - // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations + // @ts-expect-error specify aggregations type explicitly const aggregations: OverviewNetworkHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewNetworkQuery(options))], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 38fbc08b19c48..896b974ae5b84 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -76,7 +76,6 @@ export function registerSnapshotsRoutes({ const { snapshots: fetchedSnapshots = [] } = response.body; // Decorate each snapshot with the repository with which it's associated. - fetchedSnapshots.forEach((snapshot) => { snapshots.push( deserializeSnapshotDetails( diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index e88144f2b4a35..3fe003ebc6591 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -788,11 +788,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668048128, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:28.128Z', to: 1601668108128, to_as_string: '2020-10-02T19:48:28.128Z', doc_count: 0, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [], }, @@ -805,11 +805,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', from: 1601668046000, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T19:47:26.000Z', to: 1601668076000, to_as_string: '2020-10-02T19:47:56.000Z', doc_count: 3, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -883,11 +883,11 @@ describe('padBuckets', () => { padBuckets(10, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671183000, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:43.000Z', to: 1601671213000, to_as_string: '2020-10-02T20:40:13.000Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -913,11 +913,11 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', from: 1601671185793, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2020-10-02T20:39:45.793Z', to: 1601671245793, to_as_string: '2020-10-02T20:40:45.793Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { @@ -943,11 +943,11 @@ describe('padBuckets', () => { padBuckets(20, 3000, { key: '2021-02-02T10:08:32.161Z-2021-02-02T10:09:32.161Z', from: 1612260512161, - // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property from_as_string: '2021-02-02T10:08:32.161Z', to: 1612260572161, to_as_string: '2021-02-02T10:09:32.161Z', doc_count: 2, + // @ts-expect-error result type doesn't define histogram histogram: { buckets: [ { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index ce01660134683..0a8335ebe98f3 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -319,9 +319,9 @@ export class TaskStore { return { docs: tasks - // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` + // @ts-expect-error @elastic/elasticsearch _source is optional .filter((doc) => this.serializer.isRawSavedObject(doc)) - // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string` + // @ts-expect-error @elastic/elasticsearch _source is optional .map((doc) => this.serializer.rawToSavedObject(doc)) .map((doc) => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), @@ -379,10 +379,8 @@ export class TaskStore { ); return { - // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional - total, - // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional - updated, + total: total || 0, + updated: updated || 0, version_conflicts: conflictsCorrectedForContinuation, }; } catch (e) { diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index ea2bb28776ac2..aa30a60b3421c 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -208,7 +208,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { await ctx.core.elasticsearch.client.asCurrentUser.transform .putTransform({ - // @ts-expect-error @elastic/elasticsearch max_page_search_size is required in TransformPivot + // @ts-expect-error @elastic/elasticsearch group_by is expected to be optional in TransformPivot body: req.body, transform_id: transformId, }) diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index a2ba8d43c9c60..6b2849b7b9670 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -165,7 +165,7 @@ export function getResultFromEs( delete aggregations.dateAgg; } - // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets + // @ts-expect-error specify aggregations type explicitly const groupBuckets = aggregations.groupAgg?.buckets || []; const result: TimeSeriesResult = { results: [], diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index ead34f6be8850..d187228a83b17 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { EsArchiver } from '@kbn/es-archiver'; @@ -93,9 +93,12 @@ export function copyToSpaceTestSuiteFactory( }, }); + const aggs = response.aggregations as Record< + string, + estypes.AggregationsMultiBucketAggregate + >; return { - // @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`. - buckets: response.aggregations?.count.buckets as SpaceBucket[], + buckets: aggs.count.buckets, }; }; From 81ceb24bd36c96549cbbb87b076614111d2f2c56 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:14:55 -0400 Subject: [PATCH 009/118] Recommendation for known Vega-Lite issue (#102547) (#102802) Co-authored-by: Wylie Conlon --- docs/user/dashboard/vega-reference.asciidoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 327506b3eb949..167ea73652cba 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -50,6 +50,11 @@ To learn more, read about https://vega.github.io/vega/docs/specification/#autosize[autosize] in the Vega documentation. +WARNING: Autosize in Vega-Lite has https://vega.github.io/vega-lite/docs/size.html#limitations[several limitations] +that can result in a warning like `Autosize "fit" only works for single views and layered views.` +The recommended fix for this warning is to convert your spec to Vega using the <> +`VEGA_DEBUG.vega_spec` output. + [float] [[vega-theme]] ====== Default theme to match {kib} From 177caad449c9fb6ada065f08a5348e196413ac4a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 21 Jun 2021 21:16:23 +0200 Subject: [PATCH 010/118] [Exploratory View] Mobile experience (#99565) (#102773) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Bryce Buchanan Co-authored-by: Alexander Wert Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Bryce Buchanan Co-authored-by: Alexander Wert --- .../apm_observability_overview_fetchers.ts | 4 +- .../lib/observability_overview/has_data.ts | 10 +- .../settings/apm_indices/get_apm_indices.ts | 16 +- .../server/routes/observability_overview.ts | 3 +- .../plugins/observability/common/typings.ts | 13 ++ .../components/app/empty_sections/index.tsx | 10 +- .../components/app/section/apm/index.test.tsx | 2 +- .../components/app/section/apm/index.tsx | 4 +- .../components/app/section/logs/index.tsx | 4 +- .../components/app/section/metrics/index.tsx | 4 +- .../components/app/section/uptime/index.tsx | 4 +- .../components/app/section/ux/index.test.tsx | 5 +- .../components/app/section/ux/index.tsx | 7 +- .../configurations/apm/field_formats.ts | 15 +- .../configurations/constants/constants.ts | 9 +- .../constants/elasticsearch_fieldnames.ts | 2 + .../configurations/constants/labels.ts | 97 +++++++++++- .../configurations/default_configs.ts | 12 +- .../configurations/lens_attributes.ts | 92 +++++++++-- .../mobile/device_distribution_config.ts | 49 ++++++ .../mobile/distribution_config.ts | 81 ++++++++++ .../mobile/kpi_over_time_config.ts | 102 ++++++++++++ .../configurations/mobile/mobile_fields.ts | 26 ++++ .../exploratory_view.test.tsx | 2 +- .../hooks/use_app_index_pattern.tsx | 31 ++-- .../series_builder/columns/data_types_col.tsx | 1 + .../series_builder/columns/report_filters.tsx | 2 + .../series_builder/series_builder.tsx | 21 ++- .../columns/filter_expanded.test.tsx | 4 + .../series_editor/columns/filter_expanded.tsx | 39 ++++- .../series_editor/columns/series_filter.tsx | 16 +- .../series_editor/series_editor.tsx | 7 +- .../shared/exploratory_view/types.ts | 12 +- .../utils/observability_index_patterns.ts | 4 + .../public/context/has_data_context.test.tsx | 146 +++++++++++------- .../public/context/has_data_context.tsx | 76 ++++++--- .../observability/public/data_handler.test.ts | 10 +- .../public/pages/home/index.test.tsx | 42 ++--- .../public/pages/overview/index.tsx | 4 +- .../pages/overview/overview.stories.tsx | 20 ++- .../typings/fetch_overview_data/index.ts | 19 ++- 41 files changed, 822 insertions(+), 205 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/mobile_fields.ts diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 3a02efd05e5a5..ef61e25af4fc2 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -53,10 +53,8 @@ export const fetchObservabilityOverviewPageData = async ({ }; export async function getHasData() { - const res = await callApmApi({ + return await callApmApi({ endpoint: 'GET /api/apm/observability_overview/has_data', signal: null, }); - - return res.hasData; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index 5c1a33e750e12..3b6993695f3de 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -29,8 +29,14 @@ export async function getHasData({ setup }: { setup: Setup }) { 'observability_overview_has_apm_data', params ); - return response.hits.total.value > 0; + return { + hasData: response.hits.total.value > 0, + indices: setup.indices, + }; } catch (e) { - return false; + return { + hasData: false, + indices: setup.indices, + }; } } diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index d8dbc242986a6..0ade96682b362 100644 --- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -16,21 +16,11 @@ import { import { APMConfig } from '../../..'; import { APMRouteHandlerResources } from '../../../routes/typings'; import { withApmSpan } from '../../../utils/with_apm_span'; +import { ApmIndicesConfig } from '../../../../../observability/common/typings'; -type ISavedObjectsClient = Pick; +export { ApmIndicesConfig }; -export interface ApmIndicesConfig { - /* eslint-disable @typescript-eslint/naming-convention */ - 'apm_oss.sourcemapIndices': string; - 'apm_oss.errorIndices': string; - 'apm_oss.onboardingIndices': string; - 'apm_oss.spanIndices': string; - 'apm_oss.transactionIndices': string; - 'apm_oss.metricsIndices': string; - /* eslint-enable @typescript-eslint/naming-convention */ - apmAgentConfigurationIndex: string; - apmCustomLinkIndex: string; -} +type ISavedObjectsClient = Pick; export type ApmIndicesName = keyof ApmIndicesConfig; diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index d459570cf7337..c2e3d0e81ce0a 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -21,8 +21,7 @@ const observabilityOverviewHasDataRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async (resources) => { const setup = await setupRequest(resources); - const res = await getHasData({ setup }); - return { hasData: res }; + return await getHasData({ setup }); }, }); diff --git a/x-pack/plugins/observability/common/typings.ts b/x-pack/plugins/observability/common/typings.ts index bd10543ef389b..305a18903fe7e 100644 --- a/x-pack/plugins/observability/common/typings.ts +++ b/x-pack/plugins/observability/common/typings.ts @@ -10,3 +10,16 @@ export type Maybe = T | null | undefined; export const alertStatusRt = t.union([t.literal('all'), t.literal('open'), t.literal('closed')]); export type AlertStatus = t.TypeOf; + +export interface ApmIndicesConfig { + /* eslint-disable @typescript-eslint/naming-convention */ + 'apm_oss.sourcemapIndices': string; + 'apm_oss.errorIndices': string; + 'apm_oss.onboardingIndices': string; + 'apm_oss.spanIndices': string; + 'apm_oss.transactionIndices': string; + 'apm_oss.metricsIndices': string; + /* eslint-enable @typescript-eslint/naming-convention */ + apmAgentConfigurationIndex: string; + apmCustomLinkIndex: string; +} diff --git a/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx b/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx index f7ce8675d8a45..47417a2bbb545 100644 --- a/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx +++ b/x-pack/plugins/observability/public/components/app/empty_sections/index.tsx @@ -13,26 +13,24 @@ import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useHasData } from '../../../hooks/use_has_data'; import { usePluginContext } from '../../../hooks/use_plugin_context'; import { getEmptySections } from '../../../pages/overview/empty_section'; -import { UXHasDataResponse } from '../../../typings'; import { EmptySection } from './empty_section'; export function EmptySections() { const { core } = usePluginContext(); const theme = useContext(ThemeContext); - const { hasData } = useHasData(); + const { hasDataMap } = useHasData(); const appEmptySections = getEmptySections({ core }).filter(({ id }) => { if (id === 'alert') { - const { status, hasData: alerts } = hasData.alert || {}; + const { status, hasData: alerts } = hasDataMap.alert || {}; return ( status === FETCH_STATUS.FAILURE || (status === FETCH_STATUS.SUCCESS && (alerts as Alert[]).length === 0) ); } else { - const app = hasData[id]; + const app = hasDataMap[id]; if (app) { - const _hasData = id === 'ux' ? (app.hasData as UXHasDataResponse)?.hasData : app.hasData; - return app.status === FETCH_STATUS.FAILURE || !_hasData; + return app.status === FETCH_STATUS.FAILURE || !app.hasData; } } return false; diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index ad3ecd2740802..16eb8dd24d3c2 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -29,7 +29,7 @@ jest.mock('react-router-dom', () => ({ describe('APMSection', () => { beforeAll(() => { jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({ - hasData: { + hasDataMap: { apm: { status: fetcherHook.FETCH_STATUS.SUCCESS, hasData: true, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index e71468d3b028c..7a42e96c3823d 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -48,7 +48,7 @@ export function APMSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); const chartTheme = useChartTheme(); const history = useHistory(); - const { forceUpdate, hasData } = useHasData(); + const { forceUpdate, hasDataMap } = useHasData(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const { data, status } = useFetcher( @@ -66,7 +66,7 @@ export function APMSection({ bucketSize }: Props) { [bucketSize, relativeStart, relativeEnd, forceUpdate] ); - if (!hasData.apm?.hasData) { + if (!hasDataMap.apm?.hasData) { return null; } diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index cb4c831d25022..da5a8f25045a5 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -47,7 +47,7 @@ function getColorPerItem(series?: LogsFetchDataResponse['series']) { export function LogsSection({ bucketSize }: Props) { const history = useHistory(); const chartTheme = useChartTheme(); - const { forceUpdate, hasData } = useHasData(); + const { forceUpdate, hasDataMap } = useHasData(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const { data, status } = useFetcher( @@ -65,7 +65,7 @@ export function LogsSection({ bucketSize }: Props) { [bucketSize, relativeStart, relativeEnd, forceUpdate] ); - if (!hasData.infra_logs?.hasData) { + if (!hasDataMap.infra_logs?.hasData) { return null; } diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx index 5a642084733c7..2f5bb9bac9348 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -50,7 +50,7 @@ const bytesPerSecondFormatter = (value: NumberOrNull) => value === null ? '' : numeral(value).format('0b') + '/s'; export function MetricsSection({ bucketSize }: Props) { - const { forceUpdate, hasData } = useHasData(); + const { forceUpdate, hasDataMap } = useHasData(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const [sortDirection, setSortDirection] = useState('asc'); const [sortField, setSortField] = useState('uptime'); @@ -88,7 +88,7 @@ export function MetricsSection({ bucketSize }: Props) { [data, setSortField, setSortDirection] ); - if (!hasData.infra_metrics?.hasData) { + if (!hasDataMap.infra_metrics?.hasData) { return null; } diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index 1dbcdeaee800a..28cbd12663c1b 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -40,7 +40,7 @@ export function UptimeSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); const chartTheme = useChartTheme(); const history = useHistory(); - const { forceUpdate, hasData } = useHasData(); + const { forceUpdate, hasDataMap } = useHasData(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); const { data, status } = useFetcher( @@ -58,7 +58,7 @@ export function UptimeSection({ bucketSize }: Props) { [bucketSize, relativeStart, relativeEnd, forceUpdate] ); - if (!hasData.synthetics?.hasData) { + if (!hasDataMap.synthetics?.hasData) { return null; } diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index fab461476e713..61bce8aaf845d 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -28,10 +28,11 @@ jest.mock('react-router-dom', () => ({ describe('UXSection', () => { beforeAll(() => { jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({ - hasData: { + hasDataMap: { ux: { status: fetcherHook.FETCH_STATUS.SUCCESS, - hasData: { hasData: true, serviceName: 'elastic-co-frontend' }, + hasData: true, + serviceName: 'elastic-co-frontend', }, }, } as HasDataContextValue); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index 0ac337e5ba0b1..5aa89eb2d3074 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -12,7 +12,6 @@ import { getDataHandler } from '../../../../data_handler'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { useHasData } from '../../../../hooks/use_has_data'; import { useTimeRange } from '../../../../hooks/use_time_range'; -import { UXHasDataResponse } from '../../../../typings'; import CoreVitals from '../../../shared/core_web_vitals'; interface Props { @@ -20,10 +19,10 @@ interface Props { } export function UXSection({ bucketSize }: Props) { - const { forceUpdate, hasData } = useHasData(); + const { forceUpdate, hasDataMap } = useHasData(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); - const uxHasDataResponse = (hasData.ux?.hasData as UXHasDataResponse) || {}; - const serviceName = uxHasDataResponse.serviceName as string; + const uxHasDataResponse = hasDataMap.ux; + const serviceName = uxHasDataResponse?.serviceName as string; const { data, status } = useFetcher( () => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/field_formats.ts index 8d33dfbab2c62..5c1afbca2a776 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/field_formats.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/apm/field_formats.ts @@ -6,7 +6,11 @@ */ import { FieldFormat } from '../../types'; -import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames'; +import { + METRIC_SYSTEM_CPU_USAGE, + METRIC_SYSTEM_MEMORY_USAGE, + TRANSACTION_DURATION, +} from '../constants/elasticsearch_fieldnames'; export const apmFieldFormats: FieldFormat[] = [ { @@ -18,7 +22,16 @@ export const apmFieldFormats: FieldFormat[] = [ outputFormat: 'asMilliseconds', outputPrecision: 0, showSuffix: true, + useShortSuffix: true, }, }, }, + { + field: METRIC_SYSTEM_MEMORY_USAGE, + format: { id: 'bytes', params: {} }, + }, + { + field: METRIC_SYSTEM_CPU_USAGE, + format: { id: 'percent', params: {} }, + }, ]; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index 26459e676de08..e119507860c5c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -13,12 +13,13 @@ import { BROWSER_VERSION_LABEL, CLS_LABEL, CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, DEVICE_LABEL, ENVIRONMENT_LABEL, FCP_LABEL, FID_LABEL, HOST_NAME_LABEL, - KIP_OVER_TIME_LABEL, + KPI_OVER_TIME_LABEL, KPI_LABEL, LCP_LABEL, LOCATION_LABEL, @@ -31,6 +32,7 @@ import { OS_LABEL, PERF_DIST_LABEL, PORT_LABEL, + REQUEST_METHOD, SERVICE_NAME_LABEL, TAGS_LABEL, TBT_LABEL, @@ -72,14 +74,17 @@ export const FieldLabels: Record = { 'performance.metric': METRIC_LABEL, 'Business.KPI': KPI_LABEL, + 'http.request.method': REQUEST_METHOD, }; export const DataViewLabels: Record = { dist: PERF_DIST_LABEL, - kpi: KIP_OVER_TIME_LABEL, + kpi: KPI_OVER_TIME_LABEL, cwv: CORE_WEB_VITALS_LABEL, + mdd: DEVICE_DISTRIBUTION_LABEL, }; export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; +export const TERMS_COLUMN = 'TERMS_COLUMN'; export const OPERATION_COLUMN = 'operation'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts index 5ecc5b758de84..01dd2a49b9be0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts @@ -86,6 +86,8 @@ export const ERROR_PAGE_URL = 'error.page.url'; // METRICS export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; +export const METRIC_SYSTEM_MEMORY_USAGE = 'system.memory.usage'; +export const METRIC_SYSTEM_CPU_USAGE = 'system.cpu.usage'; export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total'; export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct'; export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index b5816daa419df..73739b7db12ef 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -165,7 +165,7 @@ export const KPI_LABEL = i18n.translate('xpack.observability.expView.fieldLabels export const PERF_DIST_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.performanceDistribution', { - defaultMessage: 'Performance Distribution', + defaultMessage: 'Performance distribution', } ); @@ -176,6 +176,20 @@ export const CORE_WEB_VITALS_LABEL = i18n.translate( } ); +export const DEVICE_DISTRIBUTION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.deviceDistribution', + { + defaultMessage: 'Device distribution', + } +); + +export const MOBILE_RESPONSE_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.mobileResponse', + { + defaultMessage: 'Mobile response', + } +); + export const MEMORY_USAGE_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.memoryUsage', { @@ -183,7 +197,7 @@ export const MEMORY_USAGE_LABEL = i18n.translate( } ); -export const KIP_OVER_TIME_LABEL = i18n.translate( +export const KPI_OVER_TIME_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.kpiOverTime', { defaultMessage: 'KPI over time', @@ -211,3 +225,82 @@ export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels. export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.downPings', { defaultMessage: 'Down Pings', }); + +export const CARRIER_NAME = i18n.translate('xpack.observability.expView.fieldLabels.carrierName', { + defaultMessage: 'Carrier Name', +}); + +export const REQUEST_METHOD = i18n.translate( + 'xpack.observability.expView.fieldLabels.requestMethod', + { + defaultMessage: 'Request Method', + } +); + +export const CONNECTION_TYPE = i18n.translate( + 'xpack.observability.expView.fieldLabels.connectionType', + { + defaultMessage: 'Connection Type', + } +); +export const HOST_OS = i18n.translate('xpack.observability.expView.fieldLabels.hostOS', { + defaultMessage: 'Host OS', +}); + +export const SERVICE_VERSION = i18n.translate( + 'xpack.observability.expView.fieldLabels.serviceVersion', + { + defaultMessage: 'Service Version', + } +); + +export const OS_PLATFORM = i18n.translate('xpack.observability.expView.fieldLabels.osPlatform', { + defaultMessage: 'OS Platform', +}); + +export const DEVICE_MODEL = i18n.translate('xpack.observability.expView.fieldLabels.deviceModel', { + defaultMessage: 'Device Model', +}); + +export const CARRIER_LOCATION = i18n.translate( + 'xpack.observability.expView.fieldLabels.carrierLocation', + { + defaultMessage: 'Carrier Location', + } +); + +export const RESPONSE_LATENCY = i18n.translate( + 'xpack.observability.expView.fieldLabels.responseLatency', + { + defaultMessage: 'Response latency', + } +); + +export const MOBILE_APP = i18n.translate('xpack.observability.expView.fieldLabels.mobileApp', { + defaultMessage: 'Mobile App', +}); + +export const MEMORY_USAGE = i18n.translate( + 'xpack.observability.expView.fieldLabels.mobile.memoryUsage', + { + defaultMessage: 'Memory Usage', + } +); + +export const CPU_USAGE = i18n.translate('xpack.observability.expView.fieldLabels.cpuUsage', { + defaultMessage: 'CPU Usage', +}); + +export const TRANSACTIONS_PER_MINUTE = i18n.translate( + 'xpack.observability.expView.fieldLabels.transactionPerMinute', + { + defaultMessage: 'Transactions per minute', + } +); + +export const NUMBER_OF_DEVICES = i18n.translate( + 'xpack.observability.expView.fieldLabels.numberOfDevices', + { + defaultMessage: 'Number of Devices', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 13a7900ef5764..07342d976cbea 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -12,6 +12,9 @@ import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config'; import { getKPITrendsLensConfig } from './rum/kpi_over_time_config'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; +import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; +import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; +import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; interface Props { reportType: keyof typeof ReportViewTypes; @@ -34,7 +37,14 @@ export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); - + case 'mobile': + if (reportType === 'dist') { + return getMobileKPIDistributionConfig({ indexPattern }); + } + if (reportType === 'mdd') { + return getMobileDeviceDistributionConfig({ indexPattern }); + } + return getMobileKPIConfig({ indexPattern }); default: return getKPITrendsLensConfig({ indexPattern }); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index bc535e29ab435..22ad18c663b32 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -25,13 +25,14 @@ import { FieldBasedIndexPatternColumn, SumIndexPatternColumn, TermsIndexPatternColumn, + CardinalityIndexPatternColumn, } from '../../../../../../lens/public'; import { buildPhraseFilter, buildPhrasesFilter, IndexPattern, } from '../../../../../../../../src/plugins/data/common'; -import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN } from './constants'; +import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants'; import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; function getLayerReferenceName(layerId: string) { @@ -55,6 +56,7 @@ export const parseCustomFieldName = ( let fieldName = sourceField; let columnType; let columnFilters; + let timeScale; let columnLabel; const rdf = reportViewConfig.reportDefinitions ?? []; @@ -70,17 +72,19 @@ export const parseCustomFieldName = ( ); columnType = currField?.columnType; columnFilters = currField?.columnFilters; + timeScale = currField?.timeScale; columnLabel = currField?.label; } } else if (customField.options?.[0].field || customField.options?.[0].id) { fieldName = customField.options?.[0].field || customField.options?.[0].id; columnType = customField.options?.[0].columnType; columnFilters = customField.options?.[0].columnFilters; + timeScale = customField.options?.[0].timeScale; columnLabel = customField.options?.[0].label; } } - return { fieldName, columnType, columnFilters, columnLabel }; + return { fieldName, columnType, columnFilters, timeScale, columnLabel }; }; export class LensAttributes { @@ -167,10 +171,10 @@ export class LensAttributes { this.visualization.layers[0].splitAccessor = undefined; } - getNumberRangeColumn(sourceField: string): RangeIndexPatternColumn { + getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn { return { sourceField, - label: this.reportViewConfig.labels[sourceField], + label: this.reportViewConfig.labels[sourceField] ?? label, dataType: 'number', operationType: 'range', isBucketed: true, @@ -183,6 +187,10 @@ export class LensAttributes { }; } + getCardinalityColumn(sourceField: string, label?: string) { + return this.getNumberOperationColumn(sourceField, 'unique_count', label); + } + getNumberColumn( sourceField: string, columnType?: string, @@ -190,21 +198,30 @@ export class LensAttributes { label?: string ) { if (columnType === 'operation' || operationType) { - if (operationType === 'median' || operationType === 'average' || operationType === 'sum') { + if ( + operationType === 'median' || + operationType === 'average' || + operationType === 'sum' || + operationType === 'unique_count' + ) { return this.getNumberOperationColumn(sourceField, operationType, label); } if (operationType?.includes('th')) { return this.getPercentileNumberColumn(sourceField, operationType); } } - return this.getNumberRangeColumn(sourceField); + return this.getNumberRangeColumn(sourceField, label); } getNumberOperationColumn( sourceField: string, - operationType: 'average' | 'median' | 'sum', + operationType: 'average' | 'median' | 'sum' | 'unique_count', label?: string - ): AvgIndexPatternColumn | MedianIndexPatternColumn | SumIndexPatternColumn { + ): + | AvgIndexPatternColumn + | MedianIndexPatternColumn + | SumIndexPatternColumn + | CardinalityIndexPatternColumn { return { ...buildNumberColumn(sourceField), label: @@ -247,6 +264,25 @@ export class LensAttributes { }; } + getTermsColumn(sourceField: string, label?: string): TermsIndexPatternColumn { + return { + operationType: 'terms', + sourceField, + label: label || 'Top values of ' + sourceField, + dataType: 'string', + isBucketed: true, + scale: 'ordinal', + params: { + size: 10, + orderBy: { + type: 'alphabetical', + fallback: false, + }, + orderDirection: 'desc', + }, + }; + } + getXAxis() { const { xAxisColumn } = this.reportViewConfig; @@ -263,15 +299,25 @@ export class LensAttributes { label?: string, colIndex?: number ) { - const { fieldMeta, columnType, fieldName, columnFilters, columnLabel } = this.getFieldMeta( - sourceField - ); + const { + fieldMeta, + columnType, + fieldName, + columnFilters, + timeScale, + columnLabel, + } = this.getFieldMeta(sourceField); const { type: fieldType } = fieldMeta ?? {}; + if (columnType === TERMS_COLUMN) { + return this.getTermsColumn(fieldName, columnLabel || label); + } + if (fieldName === 'Records' || columnType === FILTER_RECORDS) { return this.getRecordsColumn( columnLabel || label, - colIndex !== undefined ? columnFilters?.[colIndex] : undefined + colIndex !== undefined ? columnFilters?.[colIndex] : undefined, + timeScale ); } @@ -281,6 +327,9 @@ export class LensAttributes { if (fieldType === 'number') { return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label); } + if (operationType === 'unique_count') { + return this.getCardinalityColumn(fieldName, columnLabel || label); + } // FIXME review my approach again return this.getDateHistogramColumn(fieldName); @@ -291,13 +340,17 @@ export class LensAttributes { } getFieldMeta(sourceField: string) { - const { fieldName, columnType, columnFilters, columnLabel } = this.getCustomFieldName( - sourceField - ); + const { + fieldName, + columnType, + columnFilters, + timeScale, + columnLabel, + } = this.getCustomFieldName(sourceField); const fieldMeta = this.indexPattern.getFieldByName(fieldName); - return { fieldMeta, fieldName, columnType, columnFilters, columnLabel }; + return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel }; } getMainYAxis() { @@ -330,7 +383,11 @@ export class LensAttributes { return lensColumns; } - getRecordsColumn(label?: string, columnFilter?: ColumnFilter): CountIndexPatternColumn { + getRecordsColumn( + label?: string, + columnFilter?: ColumnFilter, + timeScale?: string + ): CountIndexPatternColumn { return { dataType: 'number', isBucketed: false, @@ -339,6 +396,7 @@ export class LensAttributes { scale: 'ratio', sourceField: 'Records', filter: columnFilter, + timeScale, } as CountIndexPatternColumn; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts new file mode 100644 index 0000000000000..6f9806660e489 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -0,0 +1,49 @@ +/* + * 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 { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { buildPhraseFilter } from '../utils'; +import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; +import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; +import { MobileFields } from './mobile_fields'; + +export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries { + return { + reportType: 'mobile-device-distribution', + defaultSeriesType: 'bar', + seriesTypes: ['bar', 'bar_horizontal'], + xAxisColumn: { + sourceField: USE_BREAK_DOWN_COLUMN, + }, + yAxisColumns: [ + { + sourceField: 'labels.device_id', + operationType: 'unique_count', + label: NUMBER_OF_DEVICES, + }, + ], + hasOperationType: false, + defaultFilters: Object.keys(MobileFields), + breakdowns: Object.keys(MobileFields), + filters: [ + ...buildPhraseFilter('agent.name', 'iOS/swift', indexPattern), + ...buildPhraseFilter('processor.event', 'transaction', indexPattern), + ], + labels: { + ...FieldLabels, + ...MobileFields, + [SERVICE_NAME]: MOBILE_APP, + }, + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts new file mode 100644 index 0000000000000..62dd38e55a32a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.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. + */ + +import { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { buildPhrasesFilter } from '../utils'; +import { + METRIC_SYSTEM_CPU_USAGE, + METRIC_SYSTEM_MEMORY_USAGE, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_DURATION, +} from '../constants/elasticsearch_fieldnames'; + +import { CPU_USAGE, MEMORY_USAGE, MOBILE_APP, RESPONSE_LATENCY } from '../constants/labels'; +import { MobileFields } from './mobile_fields'; + +export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): DataSeries { + return { + reportType: 'data-distribution', + defaultSeriesType: 'bar', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: 'performance.metric', + }, + yAxisColumns: [ + { + sourceField: RECORDS_FIELD, + }, + ], + hasOperationType: false, + defaultFilters: Object.keys(MobileFields), + breakdowns: Object.keys(MobileFields), + filters: [ + ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), + ], + labels: { + ...FieldLabels, + ...MobileFields, + [SERVICE_NAME]: MOBILE_APP, + }, + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + required: true, + }, + { + field: 'performance.metric', + custom: true, + options: [ + { + label: RESPONSE_LATENCY, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, + }, + { + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, + }, + { + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, + }, + ], + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts new file mode 100644 index 0000000000000..2ed4d95760db7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -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 { ConfigProps, DataSeries } from '../../types'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants'; +import { buildPhrasesFilter } from '../utils'; +import { + METRIC_SYSTEM_CPU_USAGE, + METRIC_SYSTEM_MEMORY_USAGE, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_DURATION, +} from '../constants/elasticsearch_fieldnames'; +import { + CPU_USAGE, + MEMORY_USAGE, + MOBILE_APP, + RESPONSE_LATENCY, + TRANSACTIONS_PER_MINUTE, +} from '../constants/labels'; +import { MobileFields } from './mobile_fields'; + +export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { + return { + reportType: 'kpi-over-time', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar', 'area'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumns: [ + { + sourceField: 'business.kpi', + operationType: 'median', + }, + ], + hasOperationType: true, + defaultFilters: Object.keys(MobileFields), + breakdowns: Object.keys(MobileFields), + filters: [ + ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), + ], + labels: { + ...FieldLabels, + ...MobileFields, + [TRANSACTION_DURATION]: RESPONSE_LATENCY, + [SERVICE_NAME]: MOBILE_APP, + [METRIC_SYSTEM_MEMORY_USAGE]: MEMORY_USAGE, + [METRIC_SYSTEM_CPU_USAGE]: CPU_USAGE, + }, + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + required: true, + }, + { + field: 'business.kpi', + custom: true, + options: [ + { + label: RESPONSE_LATENCY, + field: TRANSACTION_DURATION, + id: TRANSACTION_DURATION, + columnType: OPERATION_COLUMN, + }, + { + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, + }, + { + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, + }, + { + field: RECORDS_FIELD, + id: RECORDS_FIELD, + label: TRANSACTIONS_PER_MINUTE, + columnFilters: [ + { + language: 'kuery', + query: `processor.event: transaction`, + }, + ], + timeScale: 'm', + }, + ], + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/mobile_fields.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/mobile_fields.ts new file mode 100644 index 0000000000000..4ece4ff056a59 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/mobile_fields.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 { + CARRIER_LOCATION, + CARRIER_NAME, + CONNECTION_TYPE, + DEVICE_MODEL, + HOST_OS, + OS_PLATFORM, + SERVICE_VERSION, +} from '../constants/labels'; + +export const MobileFields: Record = { + 'host.os.platform': OS_PLATFORM, + 'host.os.full': HOST_OS, + 'service.version': SERVICE_VERSION, + 'network.carrier.icc': CARRIER_LOCATION, + 'network.carrier.name': CARRIER_NAME, + 'network.connection_type': CONNECTION_TYPE, + 'labels.device_model': DEVICE_MODEL, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 487ecdb2bafcc..779049601bd6d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -63,7 +63,7 @@ describe('ExploratoryView', () => { render(, { initSeries }); expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); - expect(await screen.findByText('Performance Distribution')).toBeInTheDocument(); + expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument(); expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument(); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 4f13cf6a1f9ca..4259bb778e511 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -12,7 +12,6 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; import { getDataHandler } from '../../../../data_handler'; -import { HasDataResponse } from '../../../../typings/fetch_overview_data'; export interface IIndexPatternContext { loading: boolean; @@ -41,17 +40,13 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { synthetics: null, ux: null, apm: null, + mobile: null, } as HasAppDataState); const { services: { data }, } = useKibana(); - const checkIfAppHasData = async (dataType: AppDataType) => { - const handler = getDataHandler(dataType); - return handler?.hasData(); - }; - const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback( async ({ dataType }) => { setSelectedApp(dataType); @@ -59,15 +54,27 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { if (hasAppData[dataType] === null) { setLoading(true); try { - const hasDataResponse = (await checkIfAppHasData(dataType)) as HasDataResponse; - - const hasDataT = hasDataResponse.hasData; - + let hasDataT = false; + let indices: string | undefined = ''; + switch (dataType) { + case 'ux': + case 'synthetics': + const resultUx = await getDataHandler(dataType)?.hasData(); + hasDataT = Boolean(resultUx?.hasData); + indices = resultUx?.indices; + break; + case 'apm': + case 'mobile': + const resultApm = await getDataHandler('apm')?.hasData(); + hasDataT = Boolean(resultApm?.hasData); + indices = resultApm?.indices['apm_oss.transactionIndices']; + break; + } setHasAppData((prevState) => ({ ...prevState, [dataType]: hasDataT })); - if (hasDataT || hasAppData?.[dataType]) { + if (hasDataT && indices) { const obsvIndexP = new ObservabilityIndexPatterns(data); - const indPattern = await obsvIndexP.getIndexPattern(dataType, hasDataResponse.indices); + const indPattern = await obsvIndexP.getIndexPattern(dataType, indices); setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern })); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 3fe88de518f75..985afdf888868 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -15,6 +15,7 @@ import { useSeriesStorage } from '../../hooks/use_series_storage'; export const dataTypes: Array<{ id: AppDataType; label: string }> = [ { id: 'synthetics', label: 'Synthetic Monitoring' }, { id: 'ux', label: 'User Experience (RUM)' }, + { id: 'mobile', label: 'Mobile Experience' }, // { id: 'infra_logs', label: 'Logs' }, // { id: 'infra_metrics', label: 'Metrics' }, // { id: 'apm', label: 'APM' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx index 9687f1bea4ec9..4571ecfe252e9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -20,8 +20,10 @@ export function ReportFilters({ ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index e24d246d60e58..9aef16931d7ec 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -18,16 +18,27 @@ import { ReportBreakdowns } from './columns/report_breakdowns'; import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { getDefaultConfigs } from '../configurations/default_configs'; +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from '../configurations/constants/labels'; export const ReportTypes: Record> = { synthetics: [ - { id: 'kpi', label: 'KPI over time' }, - { id: 'dist', label: 'Performance distribution' }, + { id: 'kpi', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', label: PERF_DIST_LABEL }, ], ux: [ - { id: 'kpi', label: 'KPI over time' }, - { id: 'dist', label: 'Performance distribution' }, - { id: 'cwv', label: 'Core Web Vitals' }, + { id: 'kpi', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', label: PERF_DIST_LABEL }, + { id: 'cwv', label: CORE_WEB_VITALS_LABEL }, + ], + mobile: [ + { id: 'kpi', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', label: PERF_DIST_LABEL }, + { id: 'mdd', label: DEVICE_DISTRIBUTION_LABEL }, ], apm: [], infra_logs: [], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index cfac838ba5aeb..2fadb0e56433e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -22,6 +22,7 @@ describe('FilterExpanded', function () { label={'Browser Family'} field={USER_AGENT_NAME} goBack={jest.fn()} + filters={[]} />, { initSeries } ); @@ -38,6 +39,7 @@ describe('FilterExpanded', function () { label={'Browser Family'} field={USER_AGENT_NAME} goBack={goBack} + filters={[]} />, { initSeries } ); @@ -64,6 +66,7 @@ describe('FilterExpanded', function () { label={'Browser Family'} field={USER_AGENT_NAME} goBack={goBack} + filters={[]} />, { initSeries } ); @@ -90,6 +93,7 @@ describe('FilterExpanded', function () { label={'Browser Family'} field={USER_AGENT_NAME} goBack={jest.fn()} + filters={[]} />, { initSeries } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 0d5b73f14671d..17d62b68c57e4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -6,17 +6,21 @@ */ import React, { useState, Fragment } from 'react'; -import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup } from '@elastic/eui'; +import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; import { i18n } from '@kbn/i18n'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { UrlFilter } from '../../types'; +import { DataSeries, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { ESFilter } from '../../../../../../../../../typings/elasticsearch'; +import { PersistableFilter } from '../../../../../../../lens/common'; +import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; interface Props { seriesId: string; @@ -25,9 +29,18 @@ interface Props { isNegated?: boolean; goBack: () => void; nestedField?: string; + filters: DataSeries['filters']; } -export function FilterExpanded({ seriesId, field, label, goBack, nestedField, isNegated }: Props) { +export function FilterExpanded({ + seriesId, + field, + label, + goBack, + nestedField, + isNegated, + filters: defaultFilters, +}: Props) { const { indexPattern } = useAppIndexPatternContext(); const [value, setValue] = useState(''); @@ -38,12 +51,25 @@ export function FilterExpanded({ seriesId, field, label, goBack, nestedField, is const series = getSeries(seriesId); + const queryFilters: ESFilter[] = []; + + defaultFilters?.forEach((qFilter: PersistableFilter | ExistsFilter) => { + if (qFilter.query) { + queryFilters.push(qFilter.query); + } + const asExistFilter = qFilter as ExistsFilter; + if (asExistFilter?.exists) { + queryFilters.push(asExistFilter.exists as QueryDslQueryContainer); + } + }); + const { values, loading } = useValuesList({ query: value, indexPatternTitle: indexPattern?.title, sourceField: field, time: series.time, keepHistory: true, + filters: queryFilters, }); const filters = series?.filters ?? []; @@ -73,6 +99,13 @@ export function FilterExpanded({ seriesId, field, label, goBack, nestedField, is /> + {displayValues.length === 0 && !loading && ( + + {i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', + })} + + )} {displayValues.map((opt) => ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx index 9e5770c2de8f9..b7e20b341b572 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -24,8 +24,10 @@ import { useSeriesStorage } from '../../hooks/use_series_storage'; interface Props { seriesId: string; defaultFilters: DataSeries['defaultFilters']; + filters: DataSeries['filters']; series: DataSeries; isNew?: boolean; + labels?: Record; } export interface Field { @@ -35,21 +37,28 @@ export interface Field { isNegated?: boolean; } -export function SeriesFilter({ series, isNew, seriesId, defaultFilters = [] }: Props) { +export function SeriesFilter({ + series, + isNew, + seriesId, + defaultFilters = [], + filters, + labels, +}: Props) { const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [selectedField, setSelectedField] = useState(); const options: Field[] = defaultFilters.map((field) => { if (typeof field === 'string') { - return { label: FieldLabels[field], field }; + return { label: labels?.[field] ?? FieldLabels[field], field }; } return { field: field.field, nested: field.nested, isNegated: field.isNegated, - label: FieldLabels[field.field], + label: labels?.[field.field] ?? FieldLabels[field.field], }; }); @@ -102,6 +111,7 @@ export function SeriesFilter({ series, isNew, seriesId, defaultFilters = [] }: P goBack={() => { setSelectedField(undefined); }} + filters={filters} /> ) : null; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 79218aa111f16..17d4356dcf65b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -49,7 +49,12 @@ export function SeriesEditor() { field: 'defaultFilters', width: '15%', render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => ( - + ), }, { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 98605dfdb4ca3..73b4d7794dd51 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -23,6 +23,7 @@ export const ReportViewTypes = { dist: 'data-distribution', kpi: 'kpi-over-time', cwv: 'core-web-vitals', + mdd: 'mobile-device-distribution', } as const; type ValueOf = T[keyof T]; @@ -45,8 +46,9 @@ export interface ReportDefinition { field?: string; label: string; description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; columnFilters?: ColumnFilter[]; + timeScale?: string; }>; } @@ -94,15 +96,15 @@ export interface ConfigProps { indexPattern: IIndexPattern; } -export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm'; +export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile'; -type FormatType = 'duration' | 'number'; +type FormatType = 'duration' | 'number' | 'bytes' | 'percent'; type InputFormat = 'microseconds' | 'milliseconds' | 'seconds'; type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize' | 'humanizePrecise'; export interface FieldFormatParams { - inputFormat: InputFormat; - outputFormat: OutputFormat; + inputFormat?: InputFormat; + outputFormat?: OutputFormat; outputPrecision?: number; showSuffix?: boolean; useShortSuffix?: boolean; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index 858eb52555da6..634408dd614da 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -23,6 +23,7 @@ const appFieldFormats: Record = { ux: rumFieldFormats, apm: apmFieldFormats, synthetics: syntheticsFieldFormats, + mobile: apmFieldFormats, }; function getFieldFormatsForApp(app: AppDataType) { @@ -35,6 +36,7 @@ export const indexPatternList: Record = { ux: 'rum_static_index_pattern_id', infra_logs: 'infra_logs_static_index_pattern_id', infra_metrics: 'infra_metrics_static_index_pattern_id', + mobile: 'mobile_static_index_pattern_id', }; const appToPatternMap: Record = { @@ -43,6 +45,7 @@ const appToPatternMap: Record = { ux: '(rum-data-view)*', infra_logs: '', infra_metrics: '', + mobile: '(mobile-data-view)*', }; const getAppIndicesWithPattern = (app: AppDataType, indices: string) => { @@ -124,6 +127,7 @@ export class ObservabilityIndexPatterns { if (!this.data) { throw new Error('data is not defined'); } + try { const indexPatternId = getAppIndexPatternId(app, indices); const indexPatternTitle = getAppIndicesWithPattern(app, indices); diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index b5a0806306461..f2f550e35ac6b 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -// import { act, getByText } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { CoreStart } from 'kibana/public'; import React from 'react'; @@ -19,10 +18,17 @@ import * as pluginContext from '../hooks/use_plugin_context'; import { PluginContextValue } from './plugin_context'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; +import { ApmIndicesConfig } from '../../common/typings'; +import { act } from '@testing-library/react'; const relativeStart = '2020-10-08T06:00:00.000Z'; const relativeEnd = '2020-10-08T07:00:00.000Z'; +const sampleAPMIndices = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'apm_oss.transactionIndices': 'apm-*', +} as ApmIndicesConfig; + function wrapper({ children }: { children: React.ReactElement }) { const history = createMemoryHistory(); return ( @@ -76,17 +82,18 @@ describe('HasDataContextProvider', () => { it('hasAnyData returns false and all apps return undefined', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toMatchObject({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { + hasDataMap: { apm: { hasData: undefined, status: 'success' }, synthetics: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, @@ -105,16 +112,16 @@ describe('HasDataContextProvider', () => { describe('all apps return false', () => { beforeAll(() => { registerApps([ - { appName: 'apm', hasData: async () => false }, + { appName: 'apm', hasData: async () => ({ hasData: false }) }, { appName: 'infra_logs', hasData: async () => false }, { appName: 'infra_metrics', hasData: async () => false }, { appName: 'synthetics', - hasData: async () => ({ hasData: false, indices: 'heartbeat-*, synthetics-*' }), + hasData: async () => ({ hasData: false }), }, { appName: 'ux', - hasData: async () => ({ hasData: false, serviceName: undefined, indices: 'apm-*' }), + hasData: async () => ({ hasData: false }), }, ]); }); @@ -124,29 +131,28 @@ describe('HasDataContextProvider', () => { it('hasAnyData returns false and all apps return false', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { + hasDataMap: { apm: { hasData: false, status: 'success' }, synthetics: { - hasData: { - hasData: false, - indices: 'heartbeat-*, synthetics-*', - }, + hasData: false, status: 'success', }, infra_logs: { hasData: false, status: 'success' }, infra_metrics: { hasData: false, status: 'success' }, ux: { - hasData: { hasData: false, serviceName: undefined, indices: 'apm-*' }, + hasData: false, status: 'success', }, alert: { hasData: [], status: 'success' }, @@ -162,7 +168,7 @@ describe('HasDataContextProvider', () => { describe('at least one app returns true', () => { beforeAll(() => { registerApps([ - { appName: 'apm', hasData: async () => true }, + { appName: 'apm', hasData: async () => ({ hasData: true }) }, { appName: 'infra_logs', hasData: async () => false }, { appName: 'infra_metrics', hasData: async () => false }, { @@ -181,29 +187,30 @@ describe('HasDataContextProvider', () => { it('hasAnyData returns true apm returns true and all other apps return false', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { + hasDataMap: { apm: { hasData: true, status: 'success' }, synthetics: { - hasData: { - hasData: false, - indices: 'heartbeat-*, synthetics-*', - }, + hasData: false, + indices: 'heartbeat-*, synthetics-*', status: 'success', }, infra_logs: { hasData: false, status: 'success' }, infra_metrics: { hasData: false, status: 'success' }, ux: { - hasData: { hasData: false, serviceName: undefined, indices: 'apm-*' }, + hasData: false, + indices: 'apm-*', status: 'success', }, alert: { hasData: [], status: 'success' }, @@ -219,7 +226,7 @@ describe('HasDataContextProvider', () => { describe('all apps return true', () => { beforeAll(() => { registerApps([ - { appName: 'apm', hasData: async () => true }, + { appName: 'apm', hasData: async () => ({ hasData: true }) }, { appName: 'infra_logs', hasData: async () => true }, { appName: 'infra_metrics', hasData: async () => true }, { @@ -238,32 +245,34 @@ describe('HasDataContextProvider', () => { it('hasAnyData returns true and all apps return true', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { + hasDataMap: { apm: { hasData: true, status: 'success', }, synthetics: { - hasData: { - hasData: true, - indices: 'heartbeat-*, synthetics-*', - }, + hasData: true, + indices: 'heartbeat-*, synthetics-*', status: 'success', }, infra_logs: { hasData: true, status: 'success' }, infra_metrics: { hasData: true, status: 'success' }, ux: { - hasData: { hasData: true, serviceName: 'ux', indices: 'apm-*' }, + hasData: true, + serviceName: 'ux', + indices: 'apm-*', status: 'success', }, alert: { hasData: [], status: 'success' }, @@ -279,7 +288,9 @@ describe('HasDataContextProvider', () => { describe('only apm is registered', () => { describe('when apm returns true', () => { beforeAll(() => { - registerApps([{ appName: 'apm', hasData: async () => true }]); + registerApps([ + { appName: 'apm', hasData: async () => ({ hasData: true, indices: sampleAPMIndices }) }, + ]); }); afterAll(unregisterAll); @@ -289,18 +300,20 @@ describe('HasDataContextProvider', () => { wrapper, }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { - apm: { hasData: true, status: 'success' }, + hasDataMap: { + apm: { hasData: true, indices: sampleAPMIndices, status: 'success' }, synthetics: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, infra_metrics: { hasData: undefined, status: 'success' }, @@ -317,7 +330,12 @@ describe('HasDataContextProvider', () => { describe('when apm returns false', () => { beforeAll(() => { - registerApps([{ appName: 'apm', hasData: async () => false }]); + registerApps([ + { + appName: 'apm', + hasData: async () => ({ indices: sampleAPMIndices, hasData: false }), + }, + ]); }); afterAll(unregisterAll); @@ -327,18 +345,24 @@ describe('HasDataContextProvider', () => { wrapper, }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { - apm: { hasData: false, status: 'success' }, + hasDataMap: { + apm: { + hasData: false, + indices: sampleAPMIndices, + status: 'success', + }, synthetics: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, infra_metrics: { hasData: undefined, status: 'success' }, @@ -381,29 +405,31 @@ describe('HasDataContextProvider', () => { it('hasAnyData returns true, apm is undefined and all other apps return true', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { + hasDataMap: { apm: { hasData: undefined, status: 'failure' }, synthetics: { - hasData: { - hasData: true, - indices: 'heartbeat-*, synthetics-*', - }, + hasData: true, + indices: 'heartbeat-*, synthetics-*', status: 'success', }, infra_logs: { hasData: true, status: 'success' }, infra_metrics: { hasData: true, status: 'success' }, ux: { - hasData: { hasData: true, serviceName: 'ux', indices: 'apm-*' }, + hasData: true, + serviceName: 'ux', + indices: 'apm-*', status: 'success', }, alert: { hasData: [], status: 'success' }, @@ -457,17 +483,19 @@ describe('HasDataContextProvider', () => { it('hasAnyData returns false and all apps return undefined', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { + hasDataMap: { apm: { hasData: undefined, status: 'failure' }, synthetics: { hasData: undefined, status: 'failure' }, infra_logs: { hasData: undefined, status: 'failure' }, @@ -505,17 +533,19 @@ describe('HasDataContextProvider', () => { it('returns all alerts available', async () => { const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper }); expect(result.current).toEqual({ - hasData: {}, + hasDataMap: {}, hasAnyData: false, isAllRequestsComplete: false, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), }); - await waitForNextUpdate(); + await act(async () => { + await waitForNextUpdate(); + }); expect(result.current).toEqual({ - hasData: { + hasDataMap: { apm: { hasData: undefined, status: 'success' }, synthetics: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 97aa72f07b09c..047a596ea349e 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -14,17 +14,23 @@ import { FETCH_STATUS } from '../hooks/use_fetcher'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useTimeRange } from '../hooks/use_time_range'; import { getObservabilityAlerts } from '../services/get_observability_alerts'; -import { ObservabilityFetchDataPlugins, UXHasDataResponse } from '../typings/fetch_overview_data'; +import { ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data'; +import { ApmIndicesConfig } from '../../common/typings'; type DataContextApps = ObservabilityFetchDataPlugins | 'alert'; export type HasDataMap = Record< DataContextApps, - { status: FETCH_STATUS; hasData?: boolean | UXHasDataResponse | Alert[] } + { + status: FETCH_STATUS; + hasData?: boolean | Alert[]; + indices?: string | ApmIndicesConfig; + serviceName?: string; + } >; export interface HasDataContextValue { - hasData: Partial; + hasDataMap: Partial; hasAnyData: boolean; isAllRequestsComplete: boolean; onRefreshTimeRange: () => void; @@ -40,7 +46,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode const [forceUpdate, setForceUpdate] = useState(''); const { absoluteStart, absoluteEnd } = useTimeRange(); - const [hasData, setHasData] = useState({}); + const [hasDataMap, setHasDataMap] = useState({}); const isExploratoryView = useRouteMatch('/exploratory-view'); @@ -49,23 +55,53 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode if (!isExploratoryView) apps.forEach(async (app) => { try { - if (app !== 'alert') { - const params = - app === 'ux' - ? { absoluteTime: { start: absoluteStart, end: absoluteEnd } } - : undefined; - - const result = await getDataHandler(app)?.hasData(params); - setHasData((prevState) => ({ + const updateState = ({ + hasData, + indices, + serviceName, + }: { + hasData?: boolean; + serviceName?: string; + indices?: string | ApmIndicesConfig; + }) => { + setHasDataMap((prevState) => ({ ...prevState, [app]: { - hasData: result, + hasData, + ...(serviceName ? { serviceName } : {}), + ...(indices ? { indices } : {}), status: FETCH_STATUS.SUCCESS, }, })); + }; + switch (app) { + case 'ux': + const params = { absoluteTime: { start: absoluteStart, end: absoluteEnd } }; + const resultUx = await getDataHandler(app)?.hasData(params); + updateState({ + hasData: resultUx?.hasData, + indices: resultUx?.indices, + serviceName: resultUx?.serviceName as string, + }); + break; + case 'synthetics': + const resultSy = await getDataHandler(app)?.hasData(); + updateState({ hasData: resultSy?.hasData, indices: resultSy?.indices }); + + break; + case 'apm': + const resultApm = await getDataHandler(app)?.hasData(); + updateState({ hasData: resultApm?.hasData, indices: resultApm?.indices }); + + break; + case 'infra_logs': + case 'infra_metrics': + const resultInfra = await getDataHandler(app)?.hasData(); + updateState({ hasData: resultInfra }); + break; } } catch (e) { - setHasData((prevState) => ({ + setHasDataMap((prevState) => ({ ...prevState, [app]: { hasData: undefined, @@ -83,7 +119,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode async function fetchAlerts() { try { const alerts = await getObservabilityAlerts({ core }); - setHasData((prevState) => ({ + setHasDataMap((prevState) => ({ ...prevState, alert: { hasData: alerts, @@ -91,7 +127,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode }, })); } catch (e) { - setHasData((prevState) => ({ + setHasDataMap((prevState) => ({ ...prevState, alert: { hasData: undefined, @@ -105,18 +141,18 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode }, [forceUpdate, core]); const isAllRequestsComplete = apps.every((app) => { - const appStatus = hasData[app]?.status; + const appStatus = hasDataMap[app]?.status; return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; }); - const hasAnyData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some( - (app) => hasData[app]?.hasData === true + const hasAnyData = (Object.keys(hasDataMap) as ObservabilityFetchDataPlugins[]).some( + (app) => hasDataMap[app]?.hasData === true ); return ( { const originalConsole = global.console; beforeAll(() => { - // mocks console to avoid poluting the test output + // mocks console to avoid polluting the test output global.console = ({ error: jest.fn() } as unknown) as typeof console; }); @@ -58,7 +64,7 @@ describe('registerDataHandler', () => { }, }; }, - hasData: async () => true, + hasData: async () => ({ hasData: true, indices: sampleAPMIndices }), }); it('registered data handler', () => { diff --git a/x-pack/plugins/observability/public/pages/home/index.test.tsx b/x-pack/plugins/observability/public/pages/home/index.test.tsx index a2c784cb4b2de..60b3e809e7de9 100644 --- a/x-pack/plugins/observability/public/pages/home/index.test.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.test.tsx @@ -24,32 +24,38 @@ describe('Home page', () => { }); it('renders loading component while requests are not returned', () => { - jest - .spyOn(hasData, 'useHasData') - .mockImplementation( - () => - ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false } as HasDataContextValue) - ); + jest.spyOn(hasData, 'useHasData').mockImplementation( + () => + ({ + hasDataMap: {}, + hasAnyData: false, + isAllRequestsComplete: false, + } as HasDataContextValue) + ); const { getByText } = render(); expect(getByText('Loading Observability')).toBeInTheDocument(); }); it('renders landing page', () => { - jest - .spyOn(hasData, 'useHasData') - .mockImplementation( - () => - ({ hasData: {}, hasAnyData: false, isAllRequestsComplete: true } as HasDataContextValue) - ); + jest.spyOn(hasData, 'useHasData').mockImplementation( + () => + ({ + hasDataMap: {}, + hasAnyData: false, + isAllRequestsComplete: true, + } as HasDataContextValue) + ); render(); expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/landing' }); }); it('renders overview page', () => { - jest - .spyOn(hasData, 'useHasData') - .mockImplementation( - () => - ({ hasData: {}, hasAnyData: true, isAllRequestsComplete: false } as HasDataContextValue) - ); + jest.spyOn(hasData, 'useHasData').mockImplementation( + () => + ({ + hasDataMap: {}, + hasAnyData: true, + isAllRequestsComplete: false, + } as HasDataContextValue) + ); render(); expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/overview' }); }); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 89398ad16f198..fdb52270befed 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -57,13 +57,13 @@ export function OverviewPage({ routeParams }: Props) { const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]); - const { hasData, hasAnyData } = useHasData(); + const { hasDataMap, hasAnyData } = useHasData(); if (hasAnyData === undefined) { return ; } - const alerts = (hasData.alert?.hasData as Alert[]) || []; + const alerts = (hasDataMap.alert?.hasData as Alert[]) || []; const { refreshInterval = 10000, refreshPaused = true } = routeParams.query; diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 2482ae7a8e7ab..dd424cf221d15 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -25,6 +25,7 @@ import { newsFeedFetchData } from './mock/news_feed.mock'; import { emptyResponse as emptyUptimeResponse, fetchUptimeData } from './mock/uptime.mock'; import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public'; +import { ApmIndicesConfig } from '../../../common/typings'; function unregisterAll() { unregisterDataHandler({ appName: 'apm' }); @@ -33,6 +34,11 @@ function unregisterAll() { unregisterDataHandler({ appName: 'synthetics' }); } +const sampleAPMIndices = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'apm_oss.transactionIndices': 'apm-*', +} as ApmIndicesConfig; + const withCore = makeDecorator({ name: 'withCore', parameterName: 'core', @@ -177,7 +183,7 @@ storiesOf('app/Overview', module) registerDataHandler({ appName: 'apm', fetchData: fetchApmData, - hasData: async () => false, + hasData: async () => ({ hasData: false, indices: sampleAPMIndices }), }); registerDataHandler({ appName: 'infra_logs', @@ -272,7 +278,7 @@ storiesOf('app/Overview', module) registerDataHandler({ appName: 'apm', fetchData: fetchApmData, - hasData: async () => true, + hasData: async () => ({ hasData: true, indices: sampleAPMIndices }), }); return ( @@ -289,7 +295,7 @@ storiesOf('app/Overview', module) registerDataHandler({ appName: 'apm', fetchData: fetchApmData, - hasData: async () => true, + hasData: async () => ({ hasData: true, indices: sampleAPMIndices }), }); registerDataHandler({ appName: 'infra_logs', @@ -321,7 +327,7 @@ storiesOf('app/Overview', module) registerDataHandler({ appName: 'apm', fetchData: fetchApmData, - hasData: async () => true, + hasData: async () => ({ hasData: true, indices: sampleAPMIndices }), }); registerDataHandler({ appName: 'infra_logs', @@ -355,7 +361,7 @@ storiesOf('app/Overview', module) registerDataHandler({ appName: 'apm', fetchData: fetchApmData, - hasData: async () => true, + hasData: async () => ({ hasData: true, indices: sampleAPMIndices }), }); registerDataHandler({ appName: 'infra_logs', @@ -386,7 +392,7 @@ storiesOf('app/Overview', module) registerDataHandler({ appName: 'apm', fetchData: async () => emptyAPMResponse, - hasData: async () => true, + hasData: async () => ({ hasData: true, indices: sampleAPMIndices }), }); registerDataHandler({ appName: 'infra_logs', @@ -420,7 +426,7 @@ storiesOf('app/Overview', module) fetchData: async () => { throw new Error('Error fetching APM data'); }, - hasData: async () => true, + hasData: async () => ({ hasData: true, indices: sampleAPMIndices }), }); registerDataHandler({ appName: 'infra_logs', diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index 6b69aa9888cf6..197a8c1060cdb 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -7,6 +7,8 @@ import { ObservabilityApp } from '../../../typings/common'; import { UXMetrics } from '../../components/shared/core_web_vitals'; +import { ApmIndicesConfig } from '../../../common/typings'; + export interface Stat { type: 'number' | 'percent' | 'bytesPerSecond'; value: number; @@ -34,11 +36,20 @@ export interface HasDataParams { export interface HasDataResponse { hasData: boolean; - indices: string; } export interface UXHasDataResponse extends HasDataResponse { - serviceName: string | number | undefined; + serviceName?: string | number; + indices?: string; +} + +export interface SyntheticsHasDataResponse extends HasDataResponse { + indices: string; +} + +export interface APMHasDataResponse { + hasData: boolean; + indices: ApmIndicesConfig; } export type FetchData = ( @@ -134,9 +145,9 @@ export interface ObservabilityFetchDataResponse { } export interface ObservabilityHasDataResponse { - apm: boolean; + apm: APMHasDataResponse; infra_metrics: boolean; infra_logs: boolean; - synthetics: HasDataResponse; + synthetics: SyntheticsHasDataResponse; ux: UXHasDataResponse; } From 9f1adeb786e6d9099127f3a386bc618f14252901 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:19:16 -0400 Subject: [PATCH 011/118] [Workplace Search] Replace library interface with EUI (#102758) (#102776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One of the Workplace Search components was using the 'react-beautiful-dnd' typing for it’s component. This PR relpaces it with the EUI equivalent. Co-authored-by: Scotty Bollinger --- .../components/display_settings/display_settings_logic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e8b419a31abb2..38424df724bd4 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,11 +5,11 @@ * 2.0. */ -import { DropResult } from 'react-beautiful-dnd'; - import { kea, MakeLogicType } from 'kea'; import { cloneDeep, isEqual, differenceBy } from 'lodash'; +import { DropResult } from '@elastic/eui'; + import { setSuccessMessage, clearFlashMessages, From dc9b56f39fd85eeb8e79d0aacd1f344068500a9e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:19:24 -0400 Subject: [PATCH 012/118] [7.x] [Exploratory View] Mobile experience (#99565) (#102774) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Bryce Buchanan Co-authored-by: Alexander Wert Co-authored-by: Shahzad Co-authored-by: Bryce Buchanan Co-authored-by: Alexander Wert From 7f836822e13bceaa6d74330b5f8349a84cd310ab Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:36:21 -0400 Subject: [PATCH 013/118] Convert EuiCards to non-shadowed defaults to avoid nested panel/shadows (#102680) (#102784) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance --- .../document_creation/document_creation_buttons.tsx | 4 ++++ 1 file changed, 4 insertions(+) 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 6d3caca87dcc3..7ed9b9ea65025 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 @@ -60,6 +60,7 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) = = ({ disabled = false }) = = ({ disabled = false }) = = ({ disabled = false }) = Date: Mon, 21 Jun 2021 16:20:37 -0400 Subject: [PATCH 014/118] [Alerting] Updating audit event terminology (#102263) (#102787) * Updating audit terminology * Fixing unit tests * Updating audit docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: ymao1 --- docs/user/security/audit-logging.asciidoc | 68 ++++++------ .../server/alerts_client/alerts_client.ts | 102 +++++++++--------- .../server/alerts_client/audit_events.test.ts | 28 ++--- .../server/alerts_client/audit_events.ts | 88 +++++++-------- .../server/alerts_client/tests/create.test.ts | 8 +- .../server/alerts_client/tests/delete.test.ts | 8 +- .../alerts_client/tests/disable.test.ts | 8 +- .../server/alerts_client/tests/enable.test.ts | 8 +- .../server/alerts_client/tests/find.test.ts | 12 +-- .../server/alerts_client/tests/get.test.ts | 8 +- .../alerts_client/tests/mute_all.test.ts | 8 +- .../alerts_client/tests/mute_instance.test.ts | 8 +- .../alerts_client/tests/unmute_all.test.ts | 8 +- .../tests/unmute_instance.test.ts | 8 +- .../server/alerts_client/tests/update.test.ts | 8 +- .../tests/update_api_key.test.ts | 8 +- 16 files changed, 193 insertions(+), 193 deletions(-) diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index b9fc0c9c4ac46..5808e56d6d289 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -93,9 +93,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is creating a connector. | `failure` | User is not authorized to create a connector. -.2+| `alert_create` -| `unknown` | User is creating an alert. -| `failure` | User is not authorized to create an alert. +.2+| `rule_create` +| `unknown` | User is creating a rule. +| `failure` | User is not authorized to create a rule. .2+| `space_create` | `unknown` | User is creating a space. @@ -128,38 +128,38 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is updating a connector. | `failure` | User is not authorized to update a connector. -.2+| `alert_update` -| `unknown` | User is updating an alert. -| `failure` | User is not authorized to update an alert. +.2+| `rule_update` +| `unknown` | User is updating a rule. +| `failure` | User is not authorized to update a rule. -.2+| `alert_update_api_key` -| `unknown` | User is updating the API key of an alert. -| `failure` | User is not authorized to update the API key of an alert. +.2+| `rule_update_api_key` +| `unknown` | User is updating the API key of a rule. +| `failure` | User is not authorized to update the API key of a rule. -.2+| `alert_enable` -| `unknown` | User is enabling an alert. -| `failure` | User is not authorized to enable an alert. +.2+| `rule_enable` +| `unknown` | User is enabling a rule. +| `failure` | User is not authorized to enable a rule. -.2+| `alert_disable` -| `unknown` | User is disabling an alert. -| `failure` | User is not authorized to disable an alert. +.2+| `rule_disable` +| `unknown` | User is disabling a rule. +| `failure` | User is not authorized to disable a rule. -.2+| `alert_mute` +.2+| `rule_mute` +| `unknown` | User is muting a rule. +| `failure` | User is not authorized to mute a rule. + +.2+| `rule_unmute` +| `unknown` | User is unmuting a rule. +| `failure` | User is not authorized to unmute a rule. + +.2+| `rule_alert_mute` | `unknown` | User is muting an alert. | `failure` | User is not authorized to mute an alert. -.2+| `alert_unmute` +.2+| `rule_alert_unmute` | `unknown` | User is unmuting an alert. | `failure` | User is not authorized to unmute an alert. -.2+| `alert_instance_mute` -| `unknown` | User is muting an alert instance. -| `failure` | User is not authorized to mute an alert instance. - -.2+| `alert_instance_unmute` -| `unknown` | User is unmuting an alert instance. -| `failure` | User is not authorized to unmute an alert instance. - .2+| `space_update` | `unknown` | User is updating a space. | `failure` | User is not authorized to update a space. @@ -183,9 +183,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is deleting a connector. | `failure` | User is not authorized to delete a connector. -.2+| `alert_delete` -| `unknown` | User is deleting an alert. -| `failure` | User is not authorized to delete an alert. +.2+| `rule_delete` +| `unknown` | User is deleting a rule. +| `failure` | User is not authorized to delete a rule. .2+| `space_delete` | `unknown` | User is deleting a space. @@ -218,13 +218,13 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | User has accessed a connector as part of a search operation. | `failure` | User is not authorized to search for connectors. -.2+| `alert_get` -| `success` | User has accessed an alert. -| `failure` | User is not authorized to access an alert. +.2+| `rule_get` +| `success` | User has accessed a rule. +| `failure` | User is not authorized to access a rule. -.2+| `alert_find` -| `success` | User has accessed an alert as part of a search operation. -| `failure` | User is not authorized to search for alerts. +.2+| `rule_find` +| `success` | User has accessed a rule as part of a search operation. +| `failure` | User is not authorized to search for rules. .2+| `space_get` | `success` | User has accessed a space. diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index c81fa7927ef7d..53d888967c431 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -63,7 +63,7 @@ import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; -import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { ruleAuditEvent, RuleAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField } from './lib'; import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; @@ -253,8 +253,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id }, error, }) @@ -305,8 +305,8 @@ export class AlertsClient { }; this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -375,8 +375,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, error, }) @@ -384,8 +384,8 @@ export class AlertsClient { throw error; } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, }) ); @@ -467,8 +467,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, error, }) ); @@ -508,8 +508,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, error, }) @@ -525,8 +525,8 @@ export class AlertsClient { authorizedData.forEach(({ id }) => this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, }) ) @@ -620,8 +620,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, savedObject: { type: 'alert', id }, error, }) @@ -630,8 +630,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -694,8 +694,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, savedObject: { type: 'alert', id }, error, }) @@ -704,8 +704,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -870,8 +870,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, savedObject: { type: 'alert', id }, error, }) @@ -900,8 +900,8 @@ export class AlertsClient { }); this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -976,8 +976,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, savedObject: { type: 'alert', id }, error, }) @@ -986,8 +986,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1090,8 +1090,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, savedObject: { type: 'alert', id }, error, }) @@ -1100,8 +1100,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1167,8 +1167,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, savedObject: { type: 'alert', id }, error, }) @@ -1177,8 +1177,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1229,8 +1229,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, savedObject: { type: 'alert', id }, error, }) @@ -1239,8 +1239,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1291,8 +1291,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1301,8 +1301,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) @@ -1358,8 +1358,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1368,8 +1368,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts index 4ccb69832cd26..781b8fe1f4715 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { AlertAuditAction, alertAuditEvent } from './audit_events'; +import { RuleAuditAction, ruleAuditEvent } from './audit_events'; -describe('#alertAuditEvent', () => { +describe('#ruleAuditEvent', () => { test('creates event with `unknown` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id: 'ALERT_ID' }, }) @@ -19,7 +19,7 @@ describe('#alertAuditEvent', () => { Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -34,22 +34,22 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User is creating alert [id=ALERT_ID]", + "message": "User is creating rule [id=ALERT_ID]", } `); }); test('creates event with `success` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, }) ).toMatchInlineSnapshot(` Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -64,15 +64,15 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User has created alert [id=ALERT_ID]", + "message": "User has created rule [id=ALERT_ID]", } `); }); test('creates event with `failure` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, error: new Error('ERROR_MESSAGE'), }) @@ -83,7 +83,7 @@ describe('#alertAuditEvent', () => { "message": "ERROR_MESSAGE", }, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -98,7 +98,7 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "Failed attempt to create alert [id=ALERT_ID]", + "message": "Failed attempt to create rule [id=ALERT_ID]", } `); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts index 93cca255d6ebc..f04b7c3701974 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts @@ -8,67 +8,67 @@ import { EcsEventOutcome, EcsEventType } from 'src/core/server'; import { AuditEvent } from '../../../security/server'; -export enum AlertAuditAction { - CREATE = 'alert_create', - GET = 'alert_get', - UPDATE = 'alert_update', - UPDATE_API_KEY = 'alert_update_api_key', - ENABLE = 'alert_enable', - DISABLE = 'alert_disable', - DELETE = 'alert_delete', - FIND = 'alert_find', - MUTE = 'alert_mute', - UNMUTE = 'alert_unmute', - MUTE_INSTANCE = 'alert_instance_mute', - UNMUTE_INSTANCE = 'alert_instance_unmute', +export enum RuleAuditAction { + CREATE = 'rule_create', + GET = 'rule_get', + UPDATE = 'rule_update', + UPDATE_API_KEY = 'rule_update_api_key', + ENABLE = 'rule_enable', + DISABLE = 'rule_disable', + DELETE = 'rule_delete', + FIND = 'rule_find', + MUTE = 'rule_mute', + UNMUTE = 'rule_unmute', + MUTE_ALERT = 'rule_alert_mute', + UNMUTE_ALERT = 'rule_alert_unmute', } type VerbsTuple = [string, string, string]; -const eventVerbs: Record = { - alert_create: ['create', 'creating', 'created'], - alert_get: ['access', 'accessing', 'accessed'], - alert_update: ['update', 'updating', 'updated'], - alert_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], - alert_enable: ['enable', 'enabling', 'enabled'], - alert_disable: ['disable', 'disabling', 'disabled'], - alert_delete: ['delete', 'deleting', 'deleted'], - alert_find: ['access', 'accessing', 'accessed'], - alert_mute: ['mute', 'muting', 'muted'], - alert_unmute: ['unmute', 'unmuting', 'unmuted'], - alert_instance_mute: ['mute instance of', 'muting instance of', 'muted instance of'], - alert_instance_unmute: ['unmute instance of', 'unmuting instance of', 'unmuted instance of'], +const eventVerbs: Record = { + rule_create: ['create', 'creating', 'created'], + rule_get: ['access', 'accessing', 'accessed'], + rule_update: ['update', 'updating', 'updated'], + rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], + rule_enable: ['enable', 'enabling', 'enabled'], + rule_disable: ['disable', 'disabling', 'disabled'], + rule_delete: ['delete', 'deleting', 'deleted'], + rule_find: ['access', 'accessing', 'accessed'], + rule_mute: ['mute', 'muting', 'muted'], + rule_unmute: ['unmute', 'unmuting', 'unmuted'], + rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'], + rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'], }; -const eventTypes: Record = { - alert_create: 'creation', - alert_get: 'access', - alert_update: 'change', - alert_update_api_key: 'change', - alert_enable: 'change', - alert_disable: 'change', - alert_delete: 'deletion', - alert_find: 'access', - alert_mute: 'change', - alert_unmute: 'change', - alert_instance_mute: 'change', - alert_instance_unmute: 'change', +const eventTypes: Record = { + rule_create: 'creation', + rule_get: 'access', + rule_update: 'change', + rule_update_api_key: 'change', + rule_enable: 'change', + rule_disable: 'change', + rule_delete: 'deletion', + rule_find: 'access', + rule_mute: 'change', + rule_unmute: 'change', + rule_alert_mute: 'change', + rule_alert_unmute: 'change', }; -export interface AlertAuditEventParams { - action: AlertAuditAction; +export interface RuleAuditEventParams { + action: RuleAuditAction; outcome?: EcsEventOutcome; savedObject?: NonNullable['saved_object']; error?: Error; } -export function alertAuditEvent({ +export function ruleAuditEvent({ action, savedObject, outcome, error, -}: AlertAuditEventParams): AuditEvent { - const doc = savedObject ? `alert [id=${savedObject.id}]` : 'an alert'; +}: RuleAuditEventParams): AuditEvent { + const doc = savedObject ? `rule [id=${savedObject.id}]` : 'a rule'; const [present, progressive, past] = eventVerbs[action]; const message = error ? `Failed attempt to ${present} ${doc}` diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index a2d5a5e0386c4..793357215d382 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -226,7 +226,7 @@ describe('create()', () => { }); describe('auditLogger', () => { - test('logs audit event when creating an alert', async () => { + test('logs audit event when creating a rule', async () => { const data = getMockData({ enabled: false, actions: [], @@ -241,7 +241,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'unknown', }), kibana: { saved_object: { id: 'mock-saved-object-id', type: 'alert' } }, @@ -249,7 +249,7 @@ describe('create()', () => { ); }); - test('logs audit event when not authorised to create an alert', async () => { + test('logs audit event when not authorised to create a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -263,7 +263,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts index 0f9d91d829854..ca0f0cf0fb5a6 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts @@ -258,12 +258,12 @@ describe('delete()', () => { }); describe('auditLogger', () => { - test('logs audit event when deleting an alert', async () => { + test('logs audit event when deleting a rule', async () => { await alertsClient.delete({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -271,14 +271,14 @@ describe('delete()', () => { ); }); - test('logs audit event when not authorised to delete an alert', async () => { + test('logs audit event when not authorised to delete a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.delete({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts index 7eb107c2f4dec..da1c5ea8bfe8d 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts @@ -126,12 +126,12 @@ describe('disable()', () => { }); describe('auditLogger', () => { - test('logs audit event when disabling an alert', async () => { + test('logs audit event when disabling a rule', async () => { await alertsClient.disable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -139,14 +139,14 @@ describe('disable()', () => { ); }); - test('logs audit event when not authorised to disable an alert', async () => { + test('logs audit event when not authorised to disable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.disable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 8329e52d7444a..b3c8d3bd83980 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -165,12 +165,12 @@ describe('enable()', () => { }); describe('auditLogger', () => { - test('logs audit event when enabling an alert', async () => { + test('logs audit event when enabling a rule', async () => { await alertsClient.enable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -178,14 +178,14 @@ describe('enable()', () => { ); }); - test('logs audit event when not authorised to enable an alert', async () => { + test('logs audit event when not authorised to enable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts index 8fa8ae7ae38b0..fe788cd43bc2b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts @@ -277,13 +277,13 @@ describe('find()', () => { }); describe('auditLogger', () => { - test('logs audit event when searching alerts', async () => { + test('logs audit event when searching rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.find(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -291,7 +291,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alerts', async () => { + test('logs audit event when not authorised to search rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized')); @@ -299,7 +299,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), error: { @@ -310,7 +310,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alert type', async () => { + test('logs audit event when not authorised to search rule type', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockResolvedValue({ ensureRuleTypeIsAuthorized: jest.fn(() => { @@ -323,7 +323,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), kibana: { saved_object: { id: '1', type: 'alert' } }, diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts index a958ea4061ae5..1be9d3e3ba2c9 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts @@ -226,13 +226,13 @@ describe('get()', () => { }); }); - test('logs audit event when getting an alert', async () => { + test('logs audit event when getting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.get({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -240,7 +240,7 @@ describe('get()', () => { ); }); - test('logs audit event when not authorised to get an alert', async () => { + test('logs audit event when not authorised to get a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); @@ -248,7 +248,7 @@ describe('get()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts index 6734ec9b99600..43f43b539ebf2 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts @@ -155,7 +155,7 @@ describe('muteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert', async () => { + test('logs audit event when muting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('muteAll()', () => { ); }); - test('logs audit event when not authorised to mute an alert', async () => { + test('logs audit event when not authorised to mute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts index bc0b7288e952f..e2e4aff61866b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts @@ -189,7 +189,7 @@ describe('muteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert instance', async () => { + test('logs audit event when muting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -209,7 +209,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -217,7 +217,7 @@ describe('muteInstance()', () => { ); }); - test('logs audit event when not authorised to mute an alert instance', async () => { + test('logs audit event when not authorised to mute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -241,7 +241,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts index c061bc7840fb6..02439d3cd6bad 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts @@ -155,7 +155,7 @@ describe('unmuteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert', async () => { + test('logs audit event when unmuting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('unmuteAll()', () => { ); }); - test('logs audit event when not authorised to unmute an alert', async () => { + test('logs audit event when not authorised to unmute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts index 4da83b6441a8d..3f3ec697a9478 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts @@ -187,7 +187,7 @@ describe('unmuteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert instance', async () => { + test('logs audit event when unmuting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -207,7 +207,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -215,7 +215,7 @@ describe('unmuteInstance()', () => { ); }); - test('logs audit event when not authorised to unmute an alert instance', async () => { + test('logs audit event when not authorised to unmute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -239,7 +239,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index c743312ef2c4b..350c9ed31298f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -1476,7 +1476,7 @@ describe('update()', () => { }); }); - test('logs audit event when updating an alert', async () => { + test('logs audit event when updating a rule', async () => { await alertsClient.update({ id: '1', data: { @@ -1495,7 +1495,7 @@ describe('update()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update', + action: 'rule_update', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -1503,7 +1503,7 @@ describe('update()', () => { ); }); - test('logs audit event when not authorised to update an alert', async () => { + test('logs audit event when not authorised to update a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -1526,7 +1526,7 @@ describe('update()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update', + action: 'rule_update', }), kibana: { saved_object: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts index 4215f14b4a560..15aa0dbc64eb8 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts @@ -295,13 +295,13 @@ describe('updateApiKey()', () => { }); describe('auditLogger', () => { - test('logs audit event when updating the API key of an alert', async () => { + test('logs audit event when updating the API key of a rule', async () => { await alertsClient.updateApiKey({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update_api_key', + action: 'rule_update_api_key', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -309,7 +309,7 @@ describe('updateApiKey()', () => { ); }); - test('logs audit event when not authorised to update the API key of an alert', async () => { + test('logs audit event when not authorised to update the API key of a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrow(); @@ -317,7 +317,7 @@ describe('updateApiKey()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update_api_key', + action: 'rule_update_api_key', }), kibana: { saved_object: { From 5b53bb207dd92f8b31dbcfd233731d1134fdf00f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 16:39:36 -0400 Subject: [PATCH 015/118] [QA] Changes the telemetry banner dismiss location (#102721) (#102792) * change telemetry banner dismiss location * use dismissBanner method instead * Update _metricbeat_dashboard.js * Update _maps.ts * Update _maps.ts * Update _metricbeat_dashboard.js Co-authored-by: Marius Dragomir --- x-pack/test/stack_functional_integration/apps/maps/_maps.ts | 5 ++--- .../apps/metricbeat/_metricbeat_dashboard.js | 5 ++--- .../apps/telemetry/_telemetry.js | 3 +++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts index 890d467f0311a..4b7b12dc5bef2 100644 --- a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts +++ b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts @@ -15,13 +15,12 @@ export default function ({ }: FtrProviderContext & { updateBaselines: boolean }) { const screenshot = getService('screenshots'); const browser = getService('browser'); - const find = getService('find'); - const PageObjects = getPageObjects(['maps']); + const PageObjects = getPageObjects(['common', 'maps']); describe('check Elastic Maps Server', function () { before(async function () { await PageObjects.maps.loadSavedMap('EMS Test'); - await find.clickByButtonText('Dismiss'); + await PageObjects.common.dismissBanner(); await browser.setScreenshotSize(1000, 1000); }); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js index 2a04d40d0b727..ac911a941c146 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js @@ -15,7 +15,6 @@ const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat'); export default function ({ getService, getPageObjects, updateBaselines }) { const screenshot = getService('screenshots'); const browser = getService('browser'); - const find = getService('find'); const log = getService('log'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']); @@ -47,7 +46,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) { // await PageObjects.dashboard.clickFullScreenMode(); await PageObjects.common.sleep(2000); - await find.clickByButtonText('Dismiss'); + await PageObjects.common.dismissBanner(); await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.common.sleep(2000); await browser.setScreenshotSize(1000, 1337); @@ -64,7 +63,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) { 'metricbeat_dashboard', updateBaselines ); - expect(percentDifference).to.be.lessThan(0.01); + expect(percentDifference).to.be.lessThan(0.017); } finally { log.debug('### Screenshot taken'); } diff --git a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js index 71133cf011cfc..d35102cae1b7f 100644 --- a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js +++ b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js @@ -18,6 +18,9 @@ export default ({ getService, getPageObjects }) => { await browser.setWindowSize(1200, 800); await PageObjects.common.navigateToApp('home'); }); + after(async function () { + await PageObjects.common.dismissBanner(); + }); it('should show banner Help us improve the Elastic Stack', async () => { const actualMessage = await PageObjects.common.getWelcomeText(); From 650ee93be0d385d7a829010d4d2211ab870aa404 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 16:48:21 -0400 Subject: [PATCH 016/118] [Lens] Error on array values in math (#102371) (#102793) * [Lens] Error on array values in math * Update error messages Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Wylie Conlon --- .../expression_functions/specs/math_column.ts | 47 ++++++++++++------- .../specs/tests/math_column.test.ts | 24 ++++++++++ .../common/expression_types/get_type.test.ts | 1 + .../common/expression_types/get_type.ts | 3 ++ .../operations/definitions/formula/math.tsx | 2 +- 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts index 0ff8faf3ce55a..633d912c29502 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -69,25 +69,40 @@ export const mathColumn: ExpressionFunctionDefinition< return id === args.id; }); if (existingColumnIndex > -1) { - throw new Error('ID must be unique'); + throw new Error( + i18n.translate('expressions.functions.mathColumn.uniqueIdError', { + defaultMessage: 'ID must be unique', + }) + ); } const newRows = input.rows.map((row) => { - return { - ...row, - [args.id]: math.fn( - { - type: 'datatable', - columns: input.columns, - rows: [row], - }, - { - expression: args.expression, - onError: args.onError, - }, - context - ), - }; + const result = math.fn( + { + type: 'datatable', + columns: input.columns, + rows: [row], + }, + { + expression: args.expression, + onError: args.onError, + }, + context + ); + + if (Array.isArray(result)) { + if (result.length === 1) { + return { ...row, [args.id]: result[0] }; + } + throw new Error( + i18n.translate('expressions.functions.mathColumn.arrayValueError', { + defaultMessage: 'Cannot perform math on array values at {name}', + values: { name: args.name }, + }) + ); + } + + return { ...row, [args.id]: result }; }); const type = newRows.length ? getType(newRows[0][args.id]) : 'null'; const newColumn: DatatableColumn = { diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts index bc6699a2b689b..e0fb0a3a9f23d 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts @@ -34,6 +34,30 @@ describe('mathColumn', () => { }); }); + it('extracts a single array value, but not a multi-value array', () => { + const arrayTable = { + ...testTable, + rows: [ + { + name: 'product1', + time: 1517842800950, // 05 Feb 2018 15:00:00 GMT + price: [605, 500], + quantity: [100], + in_stock: true, + }, + ], + }; + const args = { + id: 'output', + name: 'output', + expression: 'quantity', + }; + expect(fn(arrayTable, args).rows[0].output).toEqual(100); + expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError( + `Cannot perform math on array values` + ); + }); + it('handles onError', () => { const args = { id: 'output', diff --git a/src/plugins/expressions/common/expression_types/get_type.test.ts b/src/plugins/expressions/common/expression_types/get_type.test.ts index 6eca54d2aea44..b1a9cb703182f 100644 --- a/src/plugins/expressions/common/expression_types/get_type.test.ts +++ b/src/plugins/expressions/common/expression_types/get_type.test.ts @@ -30,6 +30,7 @@ describe('getType()', () => { }); test('throws if object has no .type property', () => { + expect(() => getType([])).toThrow(); expect(() => getType({})).toThrow(); expect(() => getType({ _type: 'foo' })).toThrow(); expect(() => getType({ tipe: 'foo' })).toThrow(); diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts index e29a610b3ed90..052508df41329 100644 --- a/src/plugins/expressions/common/expression_types/get_type.ts +++ b/src/plugins/expressions/common/expression_types/get_type.ts @@ -8,6 +8,9 @@ export function getType(node: any) { if (node == null) return 'null'; + if (Array.isArray(node)) { + throw new Error('Unexpected array value encountered.'); + } if (typeof node === 'object') { if (!node.type) throw new Error('Objects must have a type property'); return node.type; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx index 52522a18604aa..7aae35f496923 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -48,7 +48,7 @@ export const mathOperation: OperationDefinition Date: Mon, 21 Jun 2021 14:58:46 -0600 Subject: [PATCH 017/118] [Observability] [Cases] Fix Cases navigation (#102429) (#102795) --- .../components/app/cases/all_cases/index.tsx | 15 +++++--------- .../components/app/cases/case_view/index.tsx | 19 ++++++++---------- .../app/cases/create/index.test.tsx | 20 ++++++++++--------- .../components/app/cases/create/index.tsx | 15 +++++++------- .../public/pages/cases/case_details.tsx | 5 +++-- .../public/pages/cases/configure_cases.tsx | 9 +++++---- .../public/pages/cases/create_case.tsx | 9 +++++---- 7 files changed, 45 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index 1636d08aa56e4..29a16590f3eb3 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -28,19 +28,18 @@ interface AllCasesProps { export const AllCases = React.memo(({ userCanCrud }) => { const { cases: casesUi, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; const { formatUrl } = useFormatUrl(CASES_APP_ID); + const casesUrl = getUrlForApp(CASES_APP_ID); return casesUi.getAllCases({ caseDetailsNavigation: { href: ({ detailName, subCaseId }: AllCasesNavProps) => { return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId })); }, onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) => - navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: detailName, subCaseId }), - }), + navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: detailName, subCaseId })}`), }, configureCasesNavigation: { href: formatUrl(getConfigureCasesUrl()), @@ -48,9 +47,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getConfigureCasesUrl(), - }); + return navigateToUrl(`${casesUrl}${getConfigureCasesUrl()}`); }, }, createCaseNavigation: { @@ -59,9 +56,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getCreateCaseUrl(), - }); + return navigateToUrl(`${casesUrl}${getCreateCaseUrl()}`); }, }, disableAlerts: true, diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index 728333ac8c544..07d8019153a06 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -42,8 +42,10 @@ export interface CaseProps extends Props { export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { const [caseTitle, setCaseTitle] = useState(null); - const { cases: casesUi, application } = useKibana().services; - const { navigateToApp } = application; + const { + cases: casesUi, + application: { getUrlForApp, navigateToUrl }, + } = useKibana().services; const allCasesLink = getCaseUrl(); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(allCasesLink); @@ -79,6 +81,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = [caseId, formatUrl, subCaseId] ); + const casesUrl = getUrlForApp(CASES_APP_ID); return casesUi.getCaseView({ allCasesNavigation: { href: allCasesHref, @@ -86,9 +89,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: allCasesLink, - }); + return navigateToUrl(casesUrl); }, }, caseDetailsNavigation: { @@ -97,9 +98,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: caseId }), - }); + return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`); }, }, caseId, @@ -109,9 +108,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (ev != null) { ev.preventDefault(); } - return navigateToApp(`${CASES_APP_ID}`, { - path: configureCasesLink, - }); + return navigateToUrl(`${casesUrl}${configureCasesLink}`); }, }, getCaseDetailHrefWithCommentId, diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx index ec7511836328b..6dae88733fd49 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx @@ -12,7 +12,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea import { Create } from '.'; import { useKibana } from '../../../../utils/kibana_react'; import { basicCase } from '../../../../../../cases/public/containers/mock'; -import { CASES_APP_ID, CASES_OWNER } from '../constants'; +import { CASES_OWNER } from '../constants'; import { Case } from '../../../../../../cases/common'; import { getCaseDetailsUrl } from '../../../../pages/cases/links'; @@ -20,7 +20,8 @@ jest.mock('../../../../utils/kibana_react'); describe('Create case', () => { const mockCreateCase = jest.fn(); - const mockNavigateToApp = jest.fn(); + const mockNavigateToUrl = jest.fn(); + const mockCasesUrl = 'https://elastic.co/app/observability/cases'; beforeEach(() => { jest.resetAllMocks(); (useKibana as jest.Mock).mockReturnValue({ @@ -28,7 +29,7 @@ describe('Create case', () => { cases: { getCreateCase: mockCreateCase, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); }); @@ -52,7 +53,7 @@ describe('Create case', () => { onCancel(); }, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); mount( @@ -61,7 +62,7 @@ describe('Create case', () => { ); - await waitFor(() => expect(mockNavigateToApp).toHaveBeenCalledWith(`${CASES_APP_ID}`)); + await waitFor(() => expect(mockNavigateToUrl).toHaveBeenCalledWith(`${mockCasesUrl}`)); }); it('should redirect to new case when posting the case', async () => { @@ -72,7 +73,7 @@ describe('Create case', () => { onSuccess(basicCase); }, }, - application: { navigateToApp: mockNavigateToApp }, + application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl }, }, }); mount( @@ -82,9 +83,10 @@ describe('Create case', () => { ); await waitFor(() => - expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, `${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id: basicCase.id }), - }) + expect(mockNavigateToUrl).toHaveBeenNthCalledWith( + 1, + `${mockCasesUrl}${getCaseDetailsUrl({ id: basicCase.id })}` + ) ); }); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx index d7e2daea2490b..a3ed234147314 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -15,17 +15,18 @@ import { CASES_APP_ID, CASES_OWNER } from '../constants'; export const Create = React.memo(() => { const { cases, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const onSuccess = useCallback( - async ({ id }) => - navigateToApp(`${CASES_APP_ID}`, { - path: getCaseDetailsUrl({ id }), - }), - [navigateToApp] + async ({ id }) => navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id })}`), + [casesUrl, navigateToUrl] ); - const handleSetIsCancel = useCallback(() => navigateToApp(`${CASES_APP_ID}`), [navigateToApp]); + const handleSetIsCancel = useCallback(() => navigateToUrl(`${casesUrl}`), [ + casesUrl, + navigateToUrl, + ]); return ( diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index 78f1cb313ea9b..6adf5ad286808 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -16,7 +16,7 @@ import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/a export const CaseDetailsPage = React.memo(() => { const { - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ @@ -24,8 +24,9 @@ export const CaseDetailsPage = React.memo(() => { subCaseId?: string; }>(); + const casesUrl = getUrlForApp(CASES_APP_ID); if (userPermissions != null && !userPermissions.read) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index 2986c1ff34e11..a4df4855b0204 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -23,22 +23,23 @@ const ButtonEmpty = styled(EuiButtonEmpty)` function ConfigureCasesPageComponent() { const { cases, - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); const onClickGoToCases = useCallback( async (ev) => { ev.preventDefault(); - return navigateToApp(`${CASES_APP_ID}`); + return navigateToUrl(casesUrl); }, - [navigateToApp] + [casesUrl, navigateToUrl] ); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]); if (userPermissions != null && !userPermissions.read) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index 11f6d62da6103..96ed59734edda 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -25,22 +25,23 @@ export const CreateCasePage = React.memo(() => { const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); const { - application: { navigateToApp }, + application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const goTo = useCallback( async (ev) => { ev.preventDefault(); - return navigateToApp(CASES_APP_ID); + return navigateToUrl(casesUrl); }, - [navigateToApp] + [casesUrl, navigateToUrl] ); const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]); if (userPermissions != null && !userPermissions.crud) { - navigateToApp(`${CASES_APP_ID}`); + navigateToUrl(casesUrl); return null; } From 7154e51b684da88ac4c2e712d49030e393090e09 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 21 Jun 2021 23:04:48 +0200 Subject: [PATCH 018/118] [home] Fix `add sample data` page layout (#102647) (#102796) * Fix add sample data layout * remove useless change * use KibanaPageTemplate * use pageHeader for the header * move tabs inside header * tweaks * fix imports --- .../components/tutorial_directory.js | 92 +++++++------------ 1 file changed, 33 insertions(+), 59 deletions(-) diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 1fda865ebd847..d7e6c07d6dd18 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -9,27 +9,15 @@ import _ from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; +import { EuiFlexItem, EuiFlexGrid, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { Synopsis } from './synopsis'; import { SampleDataSetCards } from './sample_data_set_cards'; import { getServices } from '../kibana_services'; - -import { - EuiPage, - EuiTabs, - EuiTab, - EuiFlexItem, - EuiFlexGrid, - EuiFlexGroup, - EuiSpacer, - EuiTitle, - EuiPageBody, -} from '@elastic/eui'; - +import { KibanaPageTemplate } from '../../../../kibana_react/public'; import { getTutorials } from '../load_tutorials'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - const ALL_TAB_ID = 'all'; const SAMPLE_DATA_TAB_ID = 'sampleData'; @@ -184,17 +172,13 @@ class TutorialDirectoryUi extends React.Component { }); }; - renderTabs = () => { - return this.tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - key={index} - > - {tab.name} - - )); + getTabs = () => { + return this.tabs.map((tab) => ({ + label: tab.name, + onClick: () => this.onSelectedTabChanged(tab.id), + isSelected: tab.id === this.state.selectedTabId, + 'data-test-subj': `homeTab-${tab.id}`, + })); }; renderTabContent = () => { @@ -258,41 +242,31 @@ class TutorialDirectoryUi extends React.Component { ) : null; }; - renderHeader = () => { - const notices = this.renderNotices(); + render() { const headerLinks = this.renderHeaderLinks(); + const tabs = this.getTabs(); + const notices = this.renderNotices(); return ( - <> - - - -

- -

-
-
- {headerLinks ? {headerLinks} : null} -
- {notices} - - ); - }; - - render() { - return ( - - - {this.renderHeader()} - - {this.renderTabs()} - - {this.renderTabContent()} - - + + ), + tabs, + rightSideItems: headerLinks ? [headerLinks] : [], + }} + > + {notices && ( + <> + {notices} + + + )} + {this.renderTabContent()} + ); } } From 57eac563cb0262fd2a228c99cb7d7d0b8c26ff42 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Mon, 21 Jun 2021 17:09:01 -0400 Subject: [PATCH 019/118] Test for role specific access to non-default space (#101653) (#102803) --- .../feature_controls/spaces_security.ts | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index a91166810b626..78207c49c9b75 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -11,16 +11,23 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); - const PageObjects = getPageObjects(['common', 'settings', 'security']); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); + const spaces = getService('spaces'); const testSubjects = getService('testSubjects'); describe('security feature controls', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + await spaces.create({ + id: 'nondefaultspace', + name: 'Non-default Space', + disabledFeatures: [], + }); }); after(async () => { + await spaces.delete('nondefaultspace'); await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); }); @@ -44,8 +51,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.security.forceLogout(); await PageObjects.security.login('global_all_user', 'global_all_user-password', { - expectSpaceSelector: false, + expectSpaceSelector: true, }); + await testSubjects.click('space-card-default'); }); after(async () => { @@ -174,5 +182,56 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('managementHome'); }); }); + + // these tests are testing role specific privilege with non default space + describe('Non default space and role specific privilege', () => { + before(async () => { + await security.role.create('nondefault_space_specific_role', { + kibana: [ + { + base: ['all'], + spaces: ['nondefaultspace'], + }, + ], + }); + + await security.user.create('nondefault_space_specific_user', { + password: 'nondefault_space_specific_role-password', + roles: ['nondefault_space_specific_role'], + full_name: 'nondefaultspace_specific_user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'nondefault_space_specific_user', + 'nondefault_space_specific_role-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('nondefault_space_specific_role'), + security.user.delete('nondefault_space_specific_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows management navlink', async () => { + await PageObjects.spaceSelector.expectHomePage('nondefaultspace'); + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); + expect(navLinks).to.contain('Stack Management'); + }); + + it(`doesn't display spaces in the management section`, async () => { + await PageObjects.common.navigateToApp('management', { + basePath: '/s/nondefaultspace', + }); + await testSubjects.missingOrFail('spaces'); + }); + }); }); } From 81dc27d7c32e9ad985c1527d9516bdf16b8587ed Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:10:23 -0400 Subject: [PATCH 020/118] [App Search] Convert Engine subnav and Engine Overview pages to new page template (#102679) (#102797) * Update routers - To facilitate this being broken up into multiple PRs, non-migrated views still get a wrapping layout * Set up Engine subnav in EuiSideNav format + tweak CSS for new label - heads up that this will break the old nav, but will disappear as the pages get converted * Convert Engine Overview pages to new page template * [PR feedback] Code comment explaining side nav icon reorder Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance --- .../components/engine/engine_nav.scss | 16 +- .../components/engine/engine_nav.test.tsx | 306 +++++++++++++++++- .../components/engine/engine_nav.tsx | 261 ++++++++++++++- .../components/engine/engine_router.test.tsx | 9 +- .../components/engine/engine_router.tsx | 136 ++++---- .../engine_overview/engine_overview.test.tsx | 6 +- .../engine_overview/engine_overview.tsx | 6 +- .../engine_overview_empty.test.tsx | 12 +- .../engine_overview/engine_overview_empty.tsx | 31 +- .../engine_overview_metrics.test.tsx | 6 +- .../engine_overview_metrics.tsx | 21 +- .../app_search/components/layout/nav.test.tsx | 3 + .../app_search/components/layout/nav.tsx | 3 +- .../public/applications/app_search/index.tsx | 6 +- 14 files changed, 695 insertions(+), 127 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss index c750f63dab248..486abeb3dce4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss @@ -6,21 +6,17 @@ */ .appSearchNavEngineLabel { - padding-top: $euiSizeS; + margin-left: $euiSizeS; + padding-top: $euiSizeXS; padding-bottom: $euiSizeS; - .euiText { - font-weight: $euiFontWeightMedium; - } .euiBadge { margin-top: $euiSizeXS; } } -.appSearchNavIcons { - // EUI override - &.euiFlexItem { - flex-grow: 0; - flex-direction: row; - } +.appSearchNavIcon { + // EuiSideNav renders icons to the left of the nav link by default, but we use icons + // as warning or error indicators & prefer to render them on the right side of the nav + order: 1; } 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 c2b0a6a50fd06..015fb997c29ed 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 @@ -6,8 +6,14 @@ */ import { setMockValues } from '../../../__mocks__/kea_logic'; +import { mockUseRouteMatch } from '../../../__mocks__/react_router'; import { mockEngineValues } from '../../__mocks__'; +jest.mock('../../../shared/layout', () => ({ + ...jest.requireActual('../../../shared/layout'), // TODO: Remove once side nav components are gone + generateNavLink: jest.fn(({ to }) => ({ href: to })), +})); + import React from 'react'; import { shallow } from 'enzyme'; @@ -16,7 +22,305 @@ import { EuiBadge, EuiIcon } from '@elastic/eui'; import { rerender } from '../../../test_helpers'; -import { EngineNav } from './engine_nav'; +import { useEngineNav, EngineNav } from './engine_nav'; + +describe('useEngineNav', () => { + const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; + + beforeEach(() => { + setMockValues(values); + mockUseRouteMatch.mockReturnValue(true); + }); + + describe('returns empty', () => { + it('does not return engine nav items if not on an engine route', () => { + mockUseRouteMatch.mockReturnValueOnce(false); + expect(useEngineNav()).toBeUndefined(); + }); + + it('does not return engine nav items if data is still loading', () => { + setMockValues({ ...values, dataLoading: true }); + expect(useEngineNav()).toBeUndefined(); + }); + + it('does not return engine nav items if engine data is missing', () => { + setMockValues({ ...values, engineName: '' }); + expect(useEngineNav()).toBeUndefined(); + }); + }); + + describe('returns an array of EUI side nav items', () => { + const BASE_NAV = [ + { + id: 'engineName', + name: 'some-engine', + renderItem: expect.any(Function), + 'data-test-subj': 'EngineLabel', + }, + { + id: 'overview', + name: 'Overview', + href: '/engines/some-engine', + 'data-test-subj': 'EngineOverviewLink', + }, + ]; + + it('always returns an engine label and overview link', () => { + expect(useEngineNav()).toEqual(BASE_NAV); + }); + + describe('engine label', () => { + const renderEngineLabel = (engineNav: any) => { + return shallow(engineNav[0].renderItem() as any); + }; + + it('renders the capitalized engine name', () => { + const wrapper = renderEngineLabel(useEngineNav()); + const name = wrapper.find('.eui-textTruncate'); + + expect(name.text()).toEqual('SOME-ENGINE'); + expect(wrapper.find(EuiBadge)).toHaveLength(0); + }); + + it('renders a sample engine badge for the sample engine', () => { + setMockValues({ ...values, isSampleEngine: true }); + const wrapper = renderEngineLabel(useEngineNav()); + + expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE'); + }); + + it('renders a meta engine badge for meta engines', () => { + setMockValues({ ...values, isMetaEngine: true }); + const wrapper = renderEngineLabel(useEngineNav()); + + expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE'); + }); + }); + + it('returns an analytics nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'analytics', + name: 'Analytics', + href: '/engines/some-engine/analytics', + 'data-test-subj': 'EngineAnalyticsLink', + }, + ]); + }); + + it('returns a documents nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineDocuments: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'documents', + name: 'Documents', + href: '/engines/some-engine/documents', + 'data-test-subj': 'EngineDocumentsLink', + }, + ]); + }); + + it('returns a schema nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineSchema: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'schema', + name: 'Schema', + href: '/engines/some-engine/schema', + 'data-test-subj': 'EngineSchemaLink', + icon: expect.anything(), + }, + ]); + }); + + describe('schema nav icons', () => { + const myRole = { canViewEngineSchema: true }; + + const renderIcons = (engineNav: any) => { + return shallow(
{engineNav[2].icon}
); + }; + + it('renders schema errors alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaErrors: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaErrors"]')).toHaveLength(1); + }); + + it('renders unconfirmed schema fields info icon', () => { + setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1); + }); + + it('renders schema conflicts alert icon', () => { + setMockValues({ ...values, myRole, hasSchemaConflicts: true }); + const wrapper = renderIcons(useEngineNav()); + + expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1); + }); + }); + + describe('crawler', () => { + const myRole = { canViewEngineCrawler: true }; + + it('returns a crawler nav item', () => { + setMockValues({ ...values, myRole }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'crawler', + name: 'Web Crawler', + href: '/engines/some-engine/crawler', + 'data-test-subj': 'EngineCrawlerLink', + }, + ]); + }); + + it('does not return a crawler nav item for meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + expect(useEngineNav()).toEqual(BASE_NAV); + }); + }); + + describe('meta engine source engines', () => { + const myRole = { canViewMetaEngineSourceEngines: true }; + + it('returns a source engines nav item', () => { + setMockValues({ ...values, myRole, isMetaEngine: true }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'sourceEngines', + name: 'Engines', + href: '/engines/some-engine/engines', + 'data-test-subj': 'MetaEngineEnginesLink', + }, + ]); + }); + + it('does not return a source engines nav item for non-meta engines', () => { + setMockValues({ ...values, myRole, isMetaEngine: false }); + expect(useEngineNav()).toEqual(BASE_NAV); + }); + }); + + it('returns a relevance tuning nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'relevanceTuning', + name: 'Relevance Tuning', + href: '/engines/some-engine/relevance_tuning', + 'data-test-subj': 'EngineRelevanceTuningLink', + icon: expect.anything(), + }, + ]); + }); + + describe('relevance tuning nav icons', () => { + const myRole = { canManageEngineRelevanceTuning: true }; + + const renderIcons = (engineNav: any) => { + return shallow(
{engineNav[2].icon}
); + }; + + it('renders unconfirmed schema fields info icon', () => { + setMockValues({ ...values, myRole, engine: { unsearchedUnconfirmedFields: true } }); + const wrapper = renderIcons(useEngineNav()); + expect( + wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]') + ).toHaveLength(1); + }); + + it('renders schema conflicts alert icon', () => { + setMockValues({ ...values, myRole, engine: { invalidBoosts: true } }); + const wrapper = renderIcons(useEngineNav()); + expect( + wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]') + ).toHaveLength(1); + }); + + it('can render multiple icons', () => { + const engine = { invalidBoosts: true, unsearchedUnconfirmedFields: true }; + setMockValues({ ...values, myRole, engine }); + const wrapper = renderIcons(useEngineNav()); + expect(wrapper.find(EuiIcon)).toHaveLength(2); + }); + }); + + it('returns a synonyms nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'synonyms', + name: 'Synonyms', + href: '/engines/some-engine/synonyms', + 'data-test-subj': 'EngineSynonymsLink', + }, + ]); + }); + + it('returns a curations nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'curations', + name: 'Curations', + href: '/engines/some-engine/curations', + 'data-test-subj': 'EngineCurationsLink', + }, + ]); + }); + + it('returns a results settings nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'resultSettings', + name: 'Result Settings', + href: '/engines/some-engine/result_settings', + 'data-test-subj': 'EngineResultSettingsLink', + }, + ]); + }); + + it('returns a Search UI nav item', () => { + setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'searchUI', + name: 'Search UI', + href: '/engines/some-engine/search_ui', + 'data-test-subj': 'EngineSearchUILink', + }, + ]); + }); + + it('returns an API logs nav item', () => { + setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } }); + expect(useEngineNav()).toEqual([ + ...BASE_NAV, + { + id: 'apiLogs', + name: 'API Logs', + href: '/engines/some-engine/api_logs', + 'data-test-subj': 'EngineAPILogsLink', + }, + ]); + }); + }); +}); describe('EngineNav', () => { const values = { ...mockEngineValues, myRole: {}, dataLoading: false }; 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 0edf01bada938..76e751cf4da5f 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,13 +6,21 @@ */ import React from 'react'; +import { useRouteMatch } from 'react-router-dom'; import { useValues } from 'kea'; -import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiSideNavItemType, + EuiText, + EuiBadge, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SideNavLink, SideNavItem } from '../../../shared/layout'; +import { generateNavLink, SideNavLink, SideNavItem } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; import { ENGINE_PATH, @@ -47,6 +55,255 @@ import { EngineLogic, generateEnginePath } from './'; import './engine_nav.scss'; +export const useEngineNav = () => { + const isEngineRoute = !!useRouteMatch(ENGINE_PATH); + const { + myRole: { + canViewEngineAnalytics, + canViewEngineDocuments, + canViewEngineSchema, + canViewEngineCrawler, + canViewMetaEngineSourceEngines, + canManageEngineSynonyms, + canManageEngineCurations, + canManageEngineRelevanceTuning, + canManageEngineResultSettings, + canManageEngineSearchUi, + canViewEngineApiLogs, + }, + } = useValues(AppLogic); + const { + engineName, + dataLoading, + isSampleEngine, + isMetaEngine, + hasSchemaErrors, + hasSchemaConflicts, + hasUnconfirmedSchemaFields, + engine, + } = useValues(EngineLogic); + + if (!isEngineRoute) return undefined; + if (dataLoading) return undefined; + if (!engineName) return undefined; + + const navItems: Array> = [ + { + id: 'engineName', + name: engineName, + renderItem: () => ( + +
{engineName.toUpperCase()}
+ {isSampleEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge', { + defaultMessage: 'SAMPLE ENGINE', + })} + + )} + {isMetaEngine && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.metaEngineBadge', { + defaultMessage: 'META ENGINE', + })} + + )} +
+ ), + 'data-test-subj': 'EngineLabel', + }, + { + id: 'overview', + name: OVERVIEW_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_PATH) }), + 'data-test-subj': 'EngineOverviewLink', + }, + ]; + + if (canViewEngineAnalytics) { + navItems.push({ + id: 'analytics', + name: ANALYTICS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_ANALYTICS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineAnalyticsLink', + }); + } + + if (canViewEngineDocuments) { + navItems.push({ + id: 'documents', + name: DOCUMENTS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_DOCUMENTS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineDocumentsLink', + }); + } + + if (canViewEngineSchema) { + navItems.push({ + id: 'schema', + name: SCHEMA_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_SCHEMA_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineSchemaLink', + icon: ( + <> + {hasSchemaErrors && ( + + )} + {hasUnconfirmedSchemaFields && ( + + )} + {hasSchemaConflicts && ( + + )} + + ), + }); + } + + if (canViewEngineCrawler && !isMetaEngine) { + navItems.push({ + id: 'crawler', + name: CRAWLER_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_CRAWLER_PATH) }), + 'data-test-subj': 'EngineCrawlerLink', + }); + } + + if (canViewMetaEngineSourceEngines && isMetaEngine) { + navItems.push({ + id: 'sourceEngines', + name: ENGINES_TITLE, + ...generateNavLink({ to: generateEnginePath(META_ENGINE_SOURCE_ENGINES_PATH) }), + 'data-test-subj': 'MetaEngineEnginesLink', + }); + } + + if (canManageEngineRelevanceTuning) { + const { invalidBoosts, unsearchedUnconfirmedFields } = engine; + + navItems.push({ + id: 'relevanceTuning', + name: RELEVANCE_TUNING_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_RELEVANCE_TUNING_PATH) }), + 'data-test-subj': 'EngineRelevanceTuningLink', + icon: ( + <> + {invalidBoosts && ( + + )} + {unsearchedUnconfirmedFields && ( + + )} + + ), + }); + } + + if (canManageEngineSynonyms) { + navItems.push({ + id: 'synonyms', + name: SYNONYMS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_SYNONYMS_PATH) }), + 'data-test-subj': 'EngineSynonymsLink', + }); + } + + if (canManageEngineCurations) { + navItems.push({ + id: 'curations', + name: CURATIONS_TITLE, + ...generateNavLink({ + to: generateEnginePath(ENGINE_CURATIONS_PATH), + shouldShowActiveForSubroutes: true, + }), + 'data-test-subj': 'EngineCurationsLink', + }); + } + + if (canManageEngineResultSettings) { + navItems.push({ + id: 'resultSettings', + name: RESULT_SETTINGS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_RESULT_SETTINGS_PATH) }), + 'data-test-subj': 'EngineResultSettingsLink', + }); + } + + if (canManageEngineSearchUi) { + navItems.push({ + id: 'searchUI', + name: SEARCH_UI_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_SEARCH_UI_PATH) }), + 'data-test-subj': 'EngineSearchUILink', + }); + } + + if (canViewEngineApiLogs) { + navItems.push({ + id: 'apiLogs', + name: API_LOGS_TITLE, + ...generateNavLink({ to: generateEnginePath(ENGINE_API_LOGS_PATH) }), + 'data-test-subj': 'EngineAPILogsLink', + }); + } + + return navItems; +}; + +// TODO: Delete the below once page template migration is complete + export const EngineNav: React.FC = () => { const { myRole: { 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 cb6df5b39880a..852705583624b 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 @@ -19,7 +19,6 @@ import { Switch, Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; import { AnalyticsRouter } from '../analytics'; import { ApiLogs } from '../api_logs'; import { CrawlerRouter } from '../crawler'; @@ -80,20 +79,20 @@ describe('EngineRouter', () => { ); }); - it('renders a loading component if async data is still loading', () => { + it('renders a loading page template if async data is still loading', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); // This would happen if a user jumps around from one engine route to another. If the engine name // on the path has changed, but we still have an engine stored in state, we do not want to load // any route views as they would be rendering with the wrong data. - it('renders a loading component if the engine stored in state is stale', () => { + it('renders a loading page template if the engine stored in state is stale', () => { setMockValues({ ...values, engineName: 'some-engine' }); mockUseParams.mockReturnValue({ engineName: 'some-new-engine' }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); it('renders a default engine overview', () => { 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 40cc2ef0368c0..6510e99a000fc 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 @@ -13,11 +13,12 @@ import { useValues, useActions } from 'kea'; import { i18n } from '@kbn/i18n'; import { setQueuedErrorMessage } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; +import { Layout } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; +import { AppSearchNav } from '../../index'; import { + ENGINE_PATH, ENGINES_PATH, ENGINE_ANALYTICS_PATH, ENGINE_DOCUMENTS_PATH, @@ -38,6 +39,7 @@ import { CrawlerRouter } from '../crawler'; import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { EngineOverview } from '../engine_overview'; +import { AppSearchPageTemplate } from '../layout'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; import { SchemaRouter } from '../schema'; @@ -45,7 +47,7 @@ import { SearchUI } from '../search_ui'; import { SourceEngines } from '../source_engines'; import { Synonyms } from '../synonyms'; -import { EngineLogic, getEngineBreadcrumbs } from './'; +import { EngineLogic } from './'; export const EngineRouter: React.FC = () => { const { @@ -85,74 +87,76 @@ export const EngineRouter: React.FC = () => { } const isLoadingNewEngine = engineName !== engineNameFromUrl; - if (isLoadingNewEngine || dataLoading) return ; + if (isLoadingNewEngine || dataLoading) return ; return ( - {canViewEngineAnalytics && ( - - - - )} - {canViewEngineDocuments && ( - - - - )} - {canViewEngineDocuments && ( - - - - )} - {canViewEngineSchema && ( - - - - )} - {canManageEngineCurations && ( - - - - )} - {canManageEngineRelevanceTuning && ( - - - - )} - {canManageEngineSynonyms && ( - - - - )} - {canManageEngineResultSettings && ( - - - - )} - {canViewEngineApiLogs && ( - - - - )} - {canManageEngineSearchUi && ( - - - - )} - {canViewMetaEngineSourceEngines && ( - - - - )} - {canViewEngineCrawler && ( - - - - )} - - + + {/* TODO: Remove layout once page template migration is over */} + }> + {canViewEngineAnalytics && ( + + + + )} + {canViewEngineDocuments && ( + + + + )} + {canViewEngineDocuments && ( + + + + )} + {canViewEngineSchema && ( + + + + )} + {canManageEngineCurations && ( + + + + )} + {canManageEngineRelevanceTuning && ( + + + + )} + {canManageEngineSynonyms && ( + + + + )} + {canManageEngineResultSettings && ( + + + + )} + {canViewEngineApiLogs && ( + + + + )} + {canManageEngineSearchUi && ( + + + + )} + {canViewMetaEngineSourceEngines && ( + + + + )} + {canViewEngineCrawler && ( + + + + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index a3b2f4cfd8b9f..edacd74e046a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -12,8 +12,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; - import { EmptyEngineOverview } from './engine_overview_empty'; import { EngineOverviewMetrics } from './engine_overview_metrics'; @@ -46,10 +44,10 @@ describe('EngineOverview', () => { expect(actions.pollForOverviewMetrics).toHaveBeenCalledTimes(1); }); - it('renders a loading component if async data is still loading', () => { + it('renders a loading page template if async data is still loading', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); describe('EmptyEngineOverview', () => { 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 77552b36af239..4c15ffd8b7f94 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 @@ -9,9 +9,9 @@ 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 { AppSearchPageTemplate } from '../layout'; import { EmptyEngineOverview } from './engine_overview_empty'; @@ -32,9 +32,7 @@ export const EngineOverview: React.FC = () => { pollForOverviewMetrics(); }, []); - if (dataLoading) { - return ; - } + if (dataLoading) return ; const engineHasDocuments = documentCount > 0; const canAddDocuments = 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 ea47dc8956ddd..6750ebf1140e0 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 @@ -5,13 +5,16 @@ * 2.0. */ +import '../../__mocks__/engine_logic.mock'; + import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeader, EuiButton } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { docLinks } from '../../../shared/doc_links'; +import { getPageTitle, getPageHeaderActions } from '../../../test_helpers'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; @@ -25,12 +28,13 @@ describe('EmptyEngineOverview', () => { }); it('renders', () => { - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Engine setup'); + expect(getPageTitle(wrapper)).toEqual('Engine setup'); }); it('renders a documentation link', () => { - const header = wrapper.find(EuiPageHeader).dive().children().dive(); - expect(header.find(EuiButton).prop('href')).toEqual(`${docLinks.appSearchBase}/index.html`); + expect(getPageHeaderActions(wrapper).find(EuiButton).prop('href')).toEqual( + `${docLinks.appSearchBase}/index.html` + ); }); it('renders document creation components', () => { 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 959d544a67324..27d9c3723f126 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,35 +7,36 @@ import React from 'react'; -import { EuiPageHeader, EuiPageContentBody, EuiButton } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; import { DOCS_PREFIX } from '../../routes'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; + export const EmptyEngineOverview: React.FC = () => { return ( - <> - {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.overview.empty.headingAction', { defaultMessage: 'View documentation' } )} , - ]} - /> - - - - - - + ], + }} + > + + + ); }; 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 00ac2af219bff..620d913c5f9a7 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 @@ -5,11 +5,13 @@ * 2.0. */ +import '../../__mocks__/engine_logic.mock'; + import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; +import { getPageTitle } from '../../../test_helpers'; import { TotalStats, TotalCharts, RecentApiLogs } from './components'; import { EngineOverviewMetrics } from './engine_overview_metrics'; @@ -18,7 +20,7 @@ describe('EngineOverviewMetrics', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Engine overview'); + expect(getPageTitle(wrapper)).toEqual('Engine overview'); expect(wrapper.find(TotalStats)).toHaveLength(1); expect(wrapper.find(TotalCharts)).toHaveLength(1); expect(wrapper.find(RecentApiLogs)).toHaveLength(1); 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 2b01cfae49a20..b47ae21104ae9 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 @@ -7,23 +7,24 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { TotalStats, TotalCharts, RecentApiLogs } from './components'; export const EngineOverviewMetrics: React.FC = () => { return ( - <> - - - + }), + }} + > @@ -34,6 +35,6 @@ export const EngineOverviewMetrics: React.FC = () => { - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx index 8b06f4b26835d..80230394ce2a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx @@ -10,6 +10,9 @@ import { setMockValues } from '../../../__mocks__/kea_logic'; jest.mock('../../../shared/layout', () => ({ generateNavLink: jest.fn(({ to }) => ({ href: to })), })); +jest.mock('../engine/engine_nav', () => ({ + useEngineNav: () => [], +})); import { useAppSearchNav } from './nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx index 57fa740caebec..4737fbcf07e23 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx @@ -15,6 +15,7 @@ import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { AppLogic } from '../../app_logic'; import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, ROLE_MAPPINGS_PATH } from '../../routes'; import { CREDENTIALS_TITLE } from '../credentials'; +import { useEngineNav } from '../engine/engine_nav'; import { ENGINES_TITLE } from '../engines'; import { SETTINGS_TITLE } from '../settings'; @@ -28,7 +29,7 @@ export const useAppSearchNav = () => { id: 'engines', name: ENGINES_TITLE, ...generateNavLink({ to: ENGINES_PATH, isRoot: true }), - items: [], // TODO: Engine nav + items: useEngineNav(), }, ]; 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 b2cd3d7b54a1a..d724371cf1dc6 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 @@ -98,6 +98,9 @@ export const AppSearchConfigured: React.FC> = (props) = + + + {canManageEngines && ( @@ -116,9 +119,6 @@ export const AppSearchConfigured: React.FC> = (props) = } readOnlyMode={readOnlyMode}> - - - From 875af978284cede3a808b55cd3d865b13af214d4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:25:18 -0400 Subject: [PATCH 021/118] chore(NA): moving @kbn/storybook into bazel (#102731) (#102806) * chore(NA): moving @kbn/storybook into bazel * chore(NA): add some typing changes * chore(NA): rename typings file * chore(NA): fix typescript configs to build storybooks * chore(NA): fix eslint and typechecking Co-authored-by: Tiago Costa --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-storybook/BUILD.bazel | 98 +++++++++++++++++++ packages/kbn-storybook/package.json | 5 - packages/kbn-storybook/preset.js | 1 + packages/kbn-storybook/preset/package.json | 4 + .../{lib => }/templates/index.ejs | 0 packages/kbn-storybook/tsconfig.json | 7 +- .../{typings.d.ts => typings.ts} | 8 ++ packages/kbn-storybook/webpack.config.ts | 2 +- x-pack/package.json | 1 - yarn.lock | 2 +- 13 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 packages/kbn-storybook/BUILD.bazel create mode 100644 packages/kbn-storybook/preset/package.json rename packages/kbn-storybook/{lib => }/templates/index.ejs (100%) rename packages/kbn-storybook/{typings.d.ts => typings.ts} (56%) diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index c211751c09b49..ebab9de66032f 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -100,6 +100,7 @@ yarn kbn watch-bazel - @kbn/server-http-tools - @kbn/server-route-repository - @kbn/std +- @kbn/storybook - @kbn/telemetry-utils - @kbn/tinymath - @kbn/ui-shared-deps diff --git a/package.json b/package.json index bc040960b23dc..f1f2e233f134d 100644 --- a/package.json +++ b/package.json @@ -465,7 +465,7 @@ "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", - "@kbn/storybook": "link:packages/kbn-storybook", + "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 6208910729625..61034c562b447 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -44,6 +44,7 @@ filegroup( "//packages/kbn-server-http-tools:build", "//packages/kbn-server-route-repository:build", "//packages/kbn-std:build", + "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", "//packages/kbn-ui-shared-deps:build", diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel new file mode 100644 index 0000000000000..e18256aeb8da4 --- /dev/null +++ b/packages/kbn-storybook/BUILD.bazel @@ -0,0 +1,98 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-storybook" +PKG_REQUIRE_NAME = "@kbn/storybook" + +SOURCE_FILES = glob( + [ + "lib/**/*.ts", + "lib/**/*.tsx", + "*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "preset/package.json", + "templates/index.ejs", + "package.json", + "README.md", + "preset.js", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-ui-shared-deps", + "@npm//@storybook/addons", + "@npm//@storybook/api", + "@npm//@storybook/components", + "@npm//@storybook/core", + "@npm//@storybook/node-logger", + "@npm//@storybook/react", + "@npm//@storybook/theming", + "@npm//loader-utils", + "@npm//react", + "@npm//webpack", + "@npm//webpack-merge", +] + +TYPES_DEPS = [ + "@npm//@types/loader-utils", + "@npm//@types/node", + "@npm//@types/webpack", + "@npm//@types/webpack-merge", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index f2e4c9b3418b1..f3c12f19a0793 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -7,10 +7,5 @@ "types": "./target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" } } \ No newline at end of file diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js index c1b7195c141b4..be0012a3818b1 100644 --- a/packages/kbn-storybook/preset.js +++ b/packages/kbn-storybook/preset.js @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// eslint-disable-next-line const webpackConfig = require('./target/webpack.config').default; module.exports = { diff --git a/packages/kbn-storybook/preset/package.json b/packages/kbn-storybook/preset/package.json new file mode 100644 index 0000000000000..7cd7517d64dde --- /dev/null +++ b/packages/kbn-storybook/preset/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "main": "../preset.js" +} \ No newline at end of file diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/templates/index.ejs similarity index 100% rename from packages/kbn-storybook/lib/templates/index.ejs rename to packages/kbn-storybook/templates/index.ejs diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 586f5ea32c056..1f6886c45c505 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,14 +1,15 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "sourceRoot": "../../../../packages/kbn-storybook", + "target": "es2015", "types": ["node"] }, - "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx", "../../typings/index.d.ts"] + "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"] } diff --git a/packages/kbn-storybook/typings.d.ts b/packages/kbn-storybook/typings.ts similarity index 56% rename from packages/kbn-storybook/typings.d.ts rename to packages/kbn-storybook/typings.ts index b940de2829909..6c5d8f4da5709 100644 --- a/packages/kbn-storybook/typings.d.ts +++ b/packages/kbn-storybook/typings.ts @@ -8,3 +8,11 @@ // Storybook react doesn't declare this in its typings, but it's there. declare module '@storybook/react/standalone'; + +// Storybook references this module. It's @ts-ignored in the codebase but when +// built into its dist it strips that out. Add it here to avoid a type checking +// error. +// +// See https://github.com/storybookjs/storybook/issues/11684 +declare module 'react-syntax-highlighter/dist/cjs/create-element'; +declare module 'react-syntax-highlighter/dist/cjs/prism-light'; diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/webpack.config.ts index f25e87a7f2f4f..4af3496768734 100644 --- a/packages/kbn-storybook/webpack.config.ts +++ b/packages/kbn-storybook/webpack.config.ts @@ -94,7 +94,7 @@ export default function ({ config: storybookConfig }: { config: Configuration }) return plugin.options && typeof plugin.options.template === 'string'; }); if (htmlWebpackPlugin) { - htmlWebpackPlugin.options.template = require.resolve('../lib/templates/index.ejs'); + htmlWebpackPlugin.options.template = require.resolve('../templates/index.ejs'); } // @ts-ignore There's a long error here about the types of the diff --git a/x-pack/package.json b/x-pack/package.json index 6824a1346d6d8..59b9b21e239dd 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -27,7 +27,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", - "@kbn/storybook": "link:../packages/kbn-storybook", "@kbn/test": "link:../packages/kbn-test" }, "dependencies": { diff --git a/yarn.lock b/yarn.lock index d648c885d5b70..c5ce5fbd0265c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2768,7 +2768,7 @@ version "0.0.0" uid "" -"@kbn/storybook@link:packages/kbn-storybook": +"@kbn/storybook@link:bazel-bin/packages/kbn-storybook": version "0.0.0" uid "" From 72b434df8b1e63f6365f8f5e6e1ef94bf2a9cff6 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:44:13 -0400 Subject: [PATCH 022/118] chore(NA): remove webpack build changes for kbn/ui-shared-deps (#102780) (#102809) Co-authored-by: Tiago Costa --- packages/kbn-ui-shared-deps/webpack.config.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 438b1e0b2e77b..9d18c8033ff67 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -7,7 +7,6 @@ */ const Path = require('path'); -const Os = require('os'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); @@ -31,7 +30,8 @@ module.exports = { 'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'], }, context: __dirname, - devtool: 'cheap-source-map', + // cheap-source-map should be used if needed + devtool: false, output: { path: UiSharedDeps.distDir, filename: '[name].js', @@ -39,7 +39,6 @@ module.exports = { devtoolModuleFilenameTemplate: (info) => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', - futureEmitAssets: true, }, module: { @@ -111,7 +110,7 @@ module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin({ - parallel: Math.min(Os.cpus().length, 2), + parallel: false, minimizerOptions: { preset: [ 'default', @@ -125,7 +124,7 @@ module.exports = { cache: false, sourceMap: false, extractComments: false, - parallel: Math.min(Os.cpus().length, 2), + parallel: false, terserOptions: { compress: true, mangle: true, From 1095236dd7c1ee0a2ba72f323c8ae22daaa45f93 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:54:58 -0400 Subject: [PATCH 023/118] Upgrade apm nodejs and rum agents (#102723) (#102812) * bump apm nodejs agent version to 3.16.0 * bump apm-rum agents * use ApmConfigOptions exported from rum agent Co-authored-by: Mikhail Shustov --- package.json | 6 +-- src/core/public/apm_system.ts | 7 ++- yarn.lock | 82 +++++++++++++++-------------------- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index f1f2e233f134d..b666ea025a74f 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,8 @@ "yarn": "^1.21.1" }, "dependencies": { - "@elastic/apm-rum": "^5.6.1", - "@elastic/apm-rum-react": "^1.2.5", + "@elastic/apm-rum": "^5.8.0", + "@elastic/apm-rum-react": "^1.2.11", "@elastic/charts": "30.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@7.14.0-canary.6", @@ -221,7 +221,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.14.0", + "elastic-apm-node": "^3.16.0", "elasticsearch": "^16.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 32fc330375991..f5af7011e632e 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { ApmBase } from '@elastic/apm-rum'; +import type { ApmBase, AgentConfigOptions } from '@elastic/apm-rum'; import { modifyUrl } from '@kbn/std'; import type { InternalApplicationStart } from './application'; @@ -18,9 +18,8 @@ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OP * that lives in the Kibana Platform. */ -interface ApmConfig { - // AgentConfigOptions is not exported from @elastic/apm-rum - active?: boolean; +interface ApmConfig extends AgentConfigOptions { + // Kibana-specific config settings: globalLabels?: Record; } diff --git a/yarn.lock b/yarn.lock index c5ce5fbd0265c..313a3138df725 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1328,29 +1328,29 @@ is-absolute "^1.0.0" is-negated-glob "^1.0.0" -"@elastic/apm-rum-core@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.7.0.tgz#2213987285324781e2ebeca607f3a71245da5a84" - integrity sha512-YxfyDwlPDRy05ERb8h79eXq2ebDamlyII3sdc8zsfL6Hc1wOHK3uBGelDQjQzkUkRJqJL1Sy6LJqok2mpxQJyw== +"@elastic/apm-rum-core@^5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.11.0.tgz#6cfebb62d5ac33cf5ec9dfbe206f120ff5d17ecc" + integrity sha512-JqxsVU6/gHfWe3DiJ7uN0h0e+zFd8LbcC5i/Pa14useiKOVn4r7dHeKoWkBSJCY63cl76hotCbtgqkuVgWVzmA== dependencies: error-stack-parser "^1.3.5" opentracing "^0.14.3" promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^1.2.5": - version "1.2.5" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.5.tgz#ac715a192808e14e62e537e41b70cc8296854051" - integrity sha512-5+5Q2ztOQT0EbWFZqV2N78tcuA9qPuO5QAtSTQIYgb5lH27Sfa9G4xlTgCbJs9DzCKmhuu27E4DTArrU3tyNzA== +"@elastic/apm-rum-react@^1.2.11": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.11.tgz#945436cbe90507fda85016c0e3a44984c3f0a9c8" + integrity sha512-kl+NdNZ0eANAD7DlN3fFR7M9NeEW21rINh9aLSmEMQedUNNn+3K9oQzD4MirjV1TA5hsLSeGiCKrfPzja9Ynjw== dependencies: - "@elastic/apm-rum" "^5.6.1" + "@elastic/apm-rum" "^5.8.0" hoist-non-react-statics "^3.3.0" -"@elastic/apm-rum@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.6.1.tgz#0d1bbef774866064795f7a9c6db0c951a900de35" - integrity sha512-q6ZkDb+m2z29h6/JKqBL/nBf6/x5yYmW1vUpdW3zy03jTQp+A7LpVaPI1HNquyGryqqT/BQl4QivFcNC28pr4w== +"@elastic/apm-rum@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.8.0.tgz#ab88dc9e955b7fa2f00d5541d242a91a44c0c931" + integrity sha512-lje3SxwqhRkogCsBUsK9y0cn1Kv3dj4Ukbt4VbmNr44KRYoY9A3gTm5e5qKLF6DgsPCOc9EZBF36a0Wtjlkt/g== dependencies: - "@elastic/apm-rum-core" "^5.7.0" + "@elastic/apm-rum-core" "^5.11.0" "@elastic/app-search-javascript@^7.3.0": version "7.8.0" @@ -12159,10 +12159,10 @@ ejs@^3.1.2, ejs@^3.1.5, ejs@^3.1.6: dependencies: jake "^10.6.1" -elastic-apm-http-client@^9.8.0: - version "9.8.0" - resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.8.0.tgz#caa738c2663b3ec8521ebede86cc841e4c77863c" - integrity sha512-JrlQbijs4dY8539zH+QNKLqLDCNyNymyy720tDaj+/i5pcwWYz5ipPARAdrKkor56AmKBxib8Fd6KsSWtIYjcA== +elastic-apm-http-client@^9.8.1: + version "9.8.1" + resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.8.1.tgz#62a0352849e2d7a75696a1c777ad90ddb55083b0" + integrity sha512-tVU7+y4nSDUEZp/TXbXDxE+kXbWHsGVG1umk0OOV71UEPc/AqC7xSP5ACirOlDkewkfCOFXkvNThgu2zlx8PUw== dependencies: breadth-filter "^2.0.0" container-info "^1.0.1" @@ -12174,24 +12174,28 @@ elastic-apm-http-client@^9.8.0: stream-chopper "^3.0.1" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.14.0.tgz#942d6e86bd9d3710f51f0e43f04965d63c3fefd3" - integrity sha512-B7Xkz6UL44mm+2URdZy2yxpEB2C5CvZLOP3sGpf2h/hepXr4NgrVoRxGqO1F2b2wCB48smPv4a3v35b396VSwA== +elastic-apm-node@^3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.16.0.tgz#b55ba5c54acd2f40be704dc48c664ddb1729f20f" + integrity sha512-WR56cjpvt9ZAAw+4Ct2XjCtmy+lgn5kXZH220TRgC7W71c5uuRdioRJpIdvBPMZmeLnHwzok2+acUB7bxnYvVA== dependencies: "@elastic/ecs-pino-format" "^1.1.0" after-all-results "^2.0.0" + async-cache "^1.1.0" async-value-promise "^1.1.1" basic-auth "^2.0.1" cookie "^0.4.0" core-util-is "^1.0.2" - elastic-apm-http-client "^9.8.0" + elastic-apm-http-client "^9.8.1" end-of-stream "^1.4.4" + error-callsites "^2.0.4" error-stack-parser "^2.0.6" escape-string-regexp "^4.0.0" fast-safe-stringify "^2.0.7" http-headers "^3.0.2" is-native "^1.0.1" + load-source-map "^2.0.0" + lru-cache "^6.0.0" measured-reporting "^1.51.1" monitor-event-loop-delay "^1.0.0" object-filter-sequence "^1.0.0" @@ -12205,7 +12209,6 @@ elastic-apm-node@^3.14.0: set-cookie-serde "^1.0.0" shallow-clone-shim "^2.0.0" sql-summary "^1.0.1" - stackman "^4.0.1" traceparent "^1.0.0" traverse "^0.6.6" unicode-byte-truncate "^1.0.0" @@ -12459,10 +12462,10 @@ errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" -error-callsites@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.3.tgz#c9278de0d7d4b4861150af295bb92891393ff24a" - integrity sha512-v036z4IEffZFE5kBkV5/F2MzhLnG0vuDyN+VXpzCf4yWXvX/1WJCI0A+TGTr8HWzBfCw5k8gr9rwAo09V+obTA== +error-callsites@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.4.tgz#44f09e6a201e9a1603ead81eacac5ba258fca76e" + integrity sha512-V877Ch4FC4FN178fDK1fsrHN4I1YQIBdtjKrHh3BUHMnh3SMvwUVrqkaOgDpUuevgSNna0RBq6Ox9SGlxYrigA== error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" @@ -18328,14 +18331,12 @@ load-json-file@^6.2.0: strip-bom "^4.0.0" type-fest "^0.6.0" -load-source-map@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/load-source-map/-/load-source-map-1.0.0.tgz#318f49905ce8a709dfb7cc3f16f3efe3bcf1dd05" - integrity sha1-MY9JkFzopwnft8w/FvPv47zx3QU= +load-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-source-map/-/load-source-map-2.0.0.tgz#48f1c7002d7d9e20dd119da6e566104ec46a5683" + integrity sha512-QNZzJ2wMrTmCdeobMuMNEXHN1QGk8HG6louEkzD/zwQ7EU2RarrzlhQ4GnUYEFzLhK+Jq7IGyF/qy+XYBSO7AQ== dependencies: - in-publish "^2.0.0" - semver "^5.3.0" - source-map "^0.5.6" + source-map "^0.7.3" loader-runner@^2.4.0: version "2.4.0" @@ -25595,17 +25596,6 @@ stackframe@^1.1.0, stackframe@^1.1.1: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.1.tgz#ffef0a3318b1b60c3b58564989aca5660729ec71" integrity sha512-0PlYhdKh6AfFxRyK/v+6/k+/mMfyiEBbTM5L94D0ZytQnJ166wuwoTYLHFWGbs2dpA8Rgq763KGWmN1EQEYHRQ== -stackman@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.1.tgz#b5709446f078db9b9dadbb317f296224d9a35b5b" - integrity sha512-lntIge3BFEElgvpZT2ld5f4U+mF84fRtJ8vA3ymUVx1euVx43ZMkd09+5RWW4FmvYDFhZwPh1gvtdsdnJyF4Fg== - dependencies: - after-all-results "^2.0.0" - async-cache "^1.1.0" - debug "^4.1.1" - error-callsites "^2.0.3" - load-source-map "^1.0.0" - stacktrace-gps@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57" From 032214799b13431e4e31bafebe37af577749f6ff Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 22 Jun 2021 00:14:36 +0200 Subject: [PATCH 024/118] ILM locators (#102313) (#102814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add url service types * refactor: 💡 move locator types into its own folder * feat: 🎸 add abstract locator implementation * feat: 🎸 implement abstract locator client * feat: 🎸 add browser-side locators service * feat: 🎸 implement locator .getLocation() * feat: 🎸 implement navigate function * feat: 🎸 implement locator service in /common folder * feat: 🎸 expose locators client on browser and server * refactor: 💡 make locators async * chore: 🤖 add deprecation notice to URL generators * docs: ✏️ add deprecation notice to readme * feat: 🎸 create management app locator * refactor: 💡 simplify management locator * feat: 🎸 export management app locator from plugin contract * feat: 🎸 implement ILM locator * feat: 🎸 improve share plugin exports * feat: 🎸 improve management app locator * feat: 🎸 add useLocatorUrl React hook * feat: 🎸 add .getUrl() method to locators * feat: 🎸 migrate ILM app to use URL locators * fix: 🐛 correct typescript errors * Fix TypeScript errors in mock * Fix ILM locator unit tests * style: 💄 shorten import Co-authored-by: Vadim Kibana Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Vadim Kibana Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/management/common/index.ts | 9 +++ src/plugins/management/common/locator.test.ts | 14 ++--- src/plugins/management/common/locator.ts | 11 ++-- src/plugins/management/public/mocks/index.ts | 4 +- src/plugins/management/public/plugin.ts | 4 +- src/plugins/management/server/plugin.ts | 4 +- src/plugins/share/common/index.ts | 2 +- .../url_service/__tests__/locators.test.ts | 8 +-- .../common/url_service/__tests__/setup.ts | 5 +- .../common/url_service/locators/index.ts | 1 + .../common/url_service/locators/locator.ts | 27 ++++++++ .../common/url_service/locators/types.ts | 43 +++++++++++-- .../url_service/locators/use_locator_url.ts | 46 ++++++++++++++ .../share/common/url_service/url_service.ts | 6 +- src/plugins/share/public/index.ts | 2 + src/plugins/share/public/plugin.ts | 16 +++-- src/plugins/share/server/plugin.ts | 5 +- .../public/index.ts | 2 +- .../public/locator.ts | 61 +++++++++++++++++++ .../public/plugin.tsx | 10 ++- .../public/url_generator.ts | 61 ------------------- .../home/data_streams_tab.test.ts | 50 ++++++++++----- .../public/application/app_context.tsx | 2 +- .../{ilm_url_generator.ts => ilm_locator.ts} | 2 +- .../public/application/constants/index.ts | 2 +- .../application/mount_management_section.ts | 4 +- .../data_stream_detail_panel.tsx | 12 +--- .../template_details/tabs/tab_summary.tsx | 12 +--- .../application/services/use_ilm_locator.ts | 21 +++++++ .../application/services/use_url_generator.ts | 40 ------------ 30 files changed, 311 insertions(+), 175 deletions(-) create mode 100644 src/plugins/management/common/index.ts create mode 100644 src/plugins/share/common/url_service/locators/use_locator_url.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/locator.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/url_generator.ts rename x-pack/plugins/index_management/public/application/constants/{ilm_url_generator.ts => ilm_locator.ts} (83%) create mode 100644 x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts delete mode 100644 x-pack/plugins/index_management/public/application/services/use_url_generator.ts diff --git a/src/plugins/management/common/index.ts b/src/plugins/management/common/index.ts new file mode 100644 index 0000000000000..c701ba846bcac --- /dev/null +++ b/src/plugins/management/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ManagementAppLocator } from './locator'; diff --git a/src/plugins/management/common/locator.test.ts b/src/plugins/management/common/locator.test.ts index dda393a4203ec..20773b9732782 100644 --- a/src/plugins/management/common/locator.test.ts +++ b/src/plugins/management/common/locator.test.ts @@ -7,16 +7,16 @@ */ import { MANAGEMENT_APP_ID } from './contants'; -import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator'; +import { ManagementAppLocatorDefinition, MANAGEMENT_APP_LOCATOR } from './locator'; test('locator has the right ID', () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR); }); test('returns management app ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'a', appId: 'b', @@ -28,26 +28,26 @@ test('returns management app ID', async () => { }); test('returns Kibana location for section ID and app ID pair', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'ingest', appId: 'index', }); expect(location).toMatchObject({ - route: '/ingest/index', + path: '/ingest/index', state: {}, }); }); test('when app ID is not provided, returns path to just the section ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'data', }); expect(location).toMatchObject({ - route: '/data', + path: '/data', state: {}, }); }); diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts index 4a4a50f468adc..7dbf5e2888011 100644 --- a/src/plugins/management/common/locator.ts +++ b/src/plugins/management/common/locator.ts @@ -7,7 +7,7 @@ */ import { SerializableState } from 'src/plugins/kibana_utils/common'; -import { LocatorDefinition } from 'src/plugins/share/common'; +import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; import { MANAGEMENT_APP_ID } from './contants'; export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; @@ -17,15 +17,18 @@ export interface ManagementAppLocatorParams extends SerializableState { appId?: string; } -export class ManagementAppLocator implements LocatorDefinition { +export type ManagementAppLocator = LocatorPublic; + +export class ManagementAppLocatorDefinition + implements LocatorDefinition { public readonly id = MANAGEMENT_APP_LOCATOR; public readonly getLocation = async (params: ManagementAppLocatorParams) => { - const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; + const path = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; return { app: MANAGEMENT_APP_ID, - route, + path, state: {}, }; }; diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 70d853f32dfcc..b06e41502e9df 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -33,9 +33,11 @@ const createSetupContract = (): ManagementSetup => ({ locator: { getLocation: jest.fn(async () => ({ app: 'MANAGEMENT', - route: '', + path: '', state: {}, })), + getUrl: jest.fn(), + useUrl: jest.fn(), navigate: jest.fn(), }, }); diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 3289b2f6f5446..34719fb5070e1 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -25,7 +25,7 @@ import { } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; -import { ManagementAppLocator } from '../common/locator'; +import { ManagementAppLocatorDefinition } from '../common/locator'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -74,7 +74,7 @@ export class ManagementPlugin public setup(core: CoreSetup, { home, share }: ManagementSetupDependencies) { const kibanaVersion = this.initializerContext.env.packageInfo.version; - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); if (home) { home.featureCatalogue.register({ diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts index 349cab6206bab..cc3798d855c59 100644 --- a/src/plugins/management/server/plugin.ts +++ b/src/plugins/management/server/plugin.ts @@ -9,7 +9,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; import { LocatorPublic } from 'src/plugins/share/common'; import type { SharePluginSetup } from 'src/plugins/share/server'; -import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator'; +import { ManagementAppLocatorDefinition, ManagementAppLocatorParams } from '../common/locator'; import { capabilitiesProvider } from './capabilities_provider'; interface ManagementSetupDependencies { @@ -31,7 +31,7 @@ export class ManagementServerPlugin public setup(core: CoreSetup, { share }: ManagementSetupDependencies) { this.logger.debug('management: Setup'); - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); core.capabilities.registerProvider(capabilitiesProvider); diff --git a/src/plugins/share/common/index.ts b/src/plugins/share/common/index.ts index 8b5d8d4557194..e724117f5b7f7 100644 --- a/src/plugins/share/common/index.ts +++ b/src/plugins/share/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { LocatorDefinition, LocatorPublic } from './url_service'; +export { LocatorDefinition, LocatorPublic, useLocatorUrl } from './url_service'; diff --git a/src/plugins/share/common/url_service/__tests__/locators.test.ts b/src/plugins/share/common/url_service/__tests__/locators.test.ts index 45d727df7de48..93ba76c7399f4 100644 --- a/src/plugins/share/common/url_service/__tests__/locators.test.ts +++ b/src/plugins/share/common/url_service/__tests__/locators.test.ts @@ -53,7 +53,7 @@ describe('locators', () => { expect(location).toEqual({ app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', state: { isFlyoutOpen: true }, }); }); @@ -97,7 +97,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -130,7 +130,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -153,7 +153,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', state: { isFlyoutOpen: false, }, diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts index ad13bb8d8d216..fea3e1b945f99 100644 --- a/src/plugins/share/common/url_service/__tests__/setup.ts +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -21,7 +21,7 @@ export const testLocator: LocatorDefinition = { getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => { return { app: 'test_app', - route: `/my-object/${savedObjectId}?page=${pageNumber}`, + path: `/my-object/${savedObjectId}?page=${pageNumber}`, state: { isFlyoutOpen: showFlyout, }, @@ -34,6 +34,9 @@ export const urlServiceTestSetup = (partialDeps: Partial navigate: async () => { throw new Error('not implemented'); }, + getUrl: async () => { + throw new Error('not implemented'); + }, ...partialDeps, }; const service = new UrlService(deps); diff --git a/src/plugins/share/common/url_service/locators/index.ts b/src/plugins/share/common/url_service/locators/index.ts index f9f87215eb4db..7ab3938984f23 100644 --- a/src/plugins/share/common/url_service/locators/index.ts +++ b/src/plugins/share/common/url_service/locators/index.ts @@ -9,3 +9,4 @@ export * from './types'; export * from './locator'; export * from './locator_client'; +export { useLocatorUrl } from './use_locator_url'; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index 68c3b05a7f411..680fb2231fc48 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -7,16 +7,27 @@ */ import type { SavedObjectReference } from 'kibana/server'; +import { DependencyList } from 'react'; import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import { useLocatorUrl } from './use_locator_url'; import type { LocatorDefinition, LocatorPublic, KibanaLocation, LocatorNavigationParams, + LocatorGetUrlParams, } from './types'; export interface LocatorDependencies { + /** + * Navigate without reloading the page to a KibanaLocation. + */ navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise; + + /** + * Resolve a Kibana URL given KibanaLocation. + */ + getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } export class Locator

implements PersistableState

, LocatorPublic

{ @@ -57,13 +68,29 @@ export class Locator

implements PersistableState

return await this.definition.getLocation(params); } + public async getUrl(params: P, { absolute = false }: LocatorGetUrlParams = {}): Promise { + const location = await this.getLocation(params); + const url = this.deps.getUrl(location, { absolute }); + + return url; + } + public async navigate( params: P, { replace = false }: LocatorNavigationParams = {} ): Promise { const location = await this.getLocation(params); + await this.deps.navigate(location, { replace, }); } + + /* eslint-disable react-hooks/rules-of-hooks */ + public readonly useUrl = ( + params: P, + getUrlParams?: LocatorGetUrlParams, + deps: DependencyList = [] + ): string => useLocatorUrl

(this, params, getUrlParams, deps); + /* eslint-enable react-hooks/rules-of-hooks */ } diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts index d811ae0fd4aa2..870eaa3718d3f 100644 --- a/src/plugins/share/common/url_service/locators/types.ts +++ b/src/plugins/share/common/url_service/locators/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { DependencyList } from 'react'; import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; /** @@ -51,23 +52,57 @@ export interface LocatorDefinition

*/ export interface LocatorPublic

{ /** - * Returns a relative URL to the client-side redirect endpoint using this - * locator. (This method is necessary for compatibility with URL generators.) + * Returns a reference to a Kibana client-side location. + * + * @param params URL locator parameters. */ getLocation(params: P): Promise; + /** + * Returns a URL as a string. + * + * @param params URL locator parameters. + * @param getUrlParams URL construction parameters. + */ + getUrl(params: P, getUrlParams?: LocatorGetUrlParams): Promise; + /** * Navigate using the `core.application.navigateToApp()` method to a Kibana * location generated by this locator. This method is available only on the * browser. + * + * @param params URL locator parameters. + * @param navigationParams Navigation parameters. */ navigate(params: P, navigationParams?: LocatorNavigationParams): Promise; + + /** + * React hook which returns a URL string given locator parameters. Returns + * empty string if URL is being loaded or an error happened. + */ + useUrl: (params: P, getUrlParams?: LocatorGetUrlParams, deps?: DependencyList) => string; } +/** + * Parameters used when navigating on client-side using browser history object. + */ export interface LocatorNavigationParams { + /** + * Whether to replace a navigation entry in history queue or push a new entry. + */ replace?: boolean; } +/** + * Parameters used when constructing a string URL. + */ +export interface LocatorGetUrlParams { + /** + * Whether to return an absolute long URL or relative short URL. + */ + absolute?: boolean; +} + /** * This interface represents a location in Kibana to which one can navigate * using the `core.application.navigateToApp()` method. @@ -79,9 +114,9 @@ export interface KibanaLocation { app: string; /** - * A URL route within a Kibana application. + * A relative URL path within a Kibana application. */ - route: string; + path: string; /** * A serializable location state object, which the app can use to determine diff --git a/src/plugins/share/common/url_service/locators/use_locator_url.ts b/src/plugins/share/common/url_service/locators/use_locator_url.ts new file mode 100644 index 0000000000000..a84c712e16248 --- /dev/null +++ b/src/plugins/share/common/url_service/locators/use_locator_url.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DependencyList, useEffect, useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { LocatorGetUrlParams, LocatorPublic } from '../../../common/url_service'; + +export const useLocatorUrl =

( + locator: LocatorPublic

| null | undefined, + params: P, + getUrlParams?: LocatorGetUrlParams, + deps: DependencyList = [] +): string => { + const [url, setUrl] = useState(''); + const isMounted = useMountedState(); + + /* eslint-disable react-hooks/exhaustive-deps */ + useEffect(() => { + if (!locator) { + setUrl(''); + return; + } + + locator + .getUrl(params, getUrlParams) + .then((result: string) => { + if (!isMounted()) return; + setUrl(result); + }) + .catch((error) => { + if (!isMounted()) return; + // eslint-disable-next-line no-console + console.error('useLocatorUrl', error); + setUrl(''); + }); + }, [locator, ...deps]); + /* eslint-enable react-hooks/exhaustive-deps */ + + return url; +}; diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts index 0c3a0aabb750b..5daba1500cdfd 100644 --- a/src/plugins/share/common/url_service/url_service.ts +++ b/src/plugins/share/common/url_service/url_service.ts @@ -17,7 +17,9 @@ export class UrlService { /** * Client to work with locators. */ - locators: LocatorClient = new LocatorClient(this.deps); + public readonly locators: LocatorClient; - constructor(protected readonly deps: UrlServiceDependencies) {} + constructor(protected readonly deps: UrlServiceDependencies) { + this.locators = new LocatorClient(deps); + } } diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index d13bb15f8c72c..8f5356f6a2201 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -29,6 +29,8 @@ export { UrlGeneratorsService, } from './url_generators'; +export { useLocatorUrl } from '../common/url_service/locators/use_locator_url'; + import { SharePlugin } from './plugin'; export { KibanaURL } from './kibana_url'; diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index eb7c46cdaef86..893108b56bcfa 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -68,14 +68,22 @@ export class SharePlugin implements Plugin { core.application.register(createShortUrlRedirectApp(core, window.location)); this.url = new UrlService({ - navigate: async (location, { replace = false } = {}) => { + navigate: async ({ app, path, state }, { replace = false } = {}) => { const [start] = await core.getStartServices(); - await start.application.navigateToApp(location.app, { - path: location.route, - state: location.state, + await start.application.navigateToApp(app, { + path, + state, replace, }); }, + getUrl: async ({ app, path }, { absolute }) => { + const start = await core.getStartServices(); + const url = start[0].application.getUrlForApp(app, { + path, + absolute, + }); + return url; + }, }); return { diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 6e3c68935f77b..76e10372cdb67 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -32,7 +32,10 @@ export class SharePlugin implements Plugin { public setup(core: CoreSetup) { this.url = new UrlService({ navigate: async () => { - throw new Error('Locator .navigate() does not work on server.'); + throw new Error('Locator .navigate() currently is not supported on the server.'); + }, + getUrl: async () => { + throw new Error('Locator .getUrl() currently is not supported on the server.'); }, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/index.ts b/x-pack/plugins/index_lifecycle_management/public/index.ts index 9bfff971d5e71..cbd23a14a6114 100644 --- a/x-pack/plugins/index_lifecycle_management/public/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/index.ts @@ -14,4 +14,4 @@ export const plugin = (initializerContext: PluginInitializerContext) => { return new IndexLifecycleManagementPlugin(initializerContext); }; -export { ILM_URL_GENERATOR_ID, IlmUrlGeneratorState } from './url_generator'; +export { ILM_LOCATOR_ID, IlmLocatorParams } from './locator'; diff --git a/x-pack/plugins/index_lifecycle_management/public/locator.ts b/x-pack/plugins/index_lifecycle_management/public/locator.ts new file mode 100644 index 0000000000000..025946a095a6f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/locator.ts @@ -0,0 +1,61 @@ +/* + * 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 { SerializableState } from 'src/plugins/kibana_utils/common'; +import { ManagementAppLocator } from 'src/plugins/management/common'; +import { LocatorDefinition } from '../../../../src/plugins/share/public/'; +import { + getPoliciesListPath, + getPolicyCreatePath, + getPolicyEditPath, +} from './application/services/navigation'; +import { PLUGIN } from '../common/constants'; + +export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; + +export interface IlmLocatorParams extends SerializableState { + page: 'policies_list' | 'policy_edit' | 'policy_create'; + policyName?: string; +} + +export interface IlmLocatorDefinitionDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class IlmLocatorDefinition implements LocatorDefinition { + constructor(protected readonly deps: IlmLocatorDefinitionDependencies) {} + + public readonly id = ILM_LOCATOR_ID; + + public readonly getLocation = async (params: IlmLocatorParams) => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'data', + appId: PLUGIN.ID, + }); + + switch (params.page) { + case 'policy_create': { + return { + ...location, + path: location.path + getPolicyCreatePath(), + }; + } + case 'policy_edit': { + return { + ...location, + path: location.path + getPolicyEditPath(params.policyName!), + }; + } + case 'policies_list': { + return { + ...location, + path: location.path + getPoliciesListPath(), + }; + } + } + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 069d1e0d10e0b..163fe2b3d9b5c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -17,7 +17,7 @@ import { init as initNotification } from './application/services/notification'; import { BreadcrumbService } from './application/services/breadcrumbs'; import { addAllExtensions } from './extend_index_management'; import { ClientConfigType, SetupDependencies, StartDependencies } from './types'; -import { registerUrlGenerator } from './url_generator'; +import { IlmLocatorDefinition } from './locator'; export class IndexLifecycleManagementPlugin implements Plugin { @@ -38,7 +38,7 @@ export class IndexLifecycleManagementPlugin getStartServices, } = coreSetup; - const { usageCollection, management, indexManagement, home, cloud, share } = plugins; + const { usageCollection, management, indexManagement, home, cloud } = plugins; // Initialize services even if the app isn't mounted, because they're used by index management extensions. initHttp(http); @@ -110,7 +110,11 @@ export class IndexLifecycleManagementPlugin addAllExtensions(indexManagement.extensionsService); } - registerUrlGenerator(coreSetup, management, share); + plugins.share.url.locators.create( + new IlmLocatorDefinition({ + managementAppLocator: plugins.management.locator, + }) + ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts b/x-pack/plugins/index_lifecycle_management/public/url_generator.ts deleted file mode 100644 index f7794c535198f..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup } from 'kibana/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public/'; -import { - getPoliciesListPath, - getPolicyCreatePath, - getPolicyEditPath, -} from './application/services/navigation'; -import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; -import { SetupDependencies } from './types'; -import { PLUGIN } from '../common/constants'; - -export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID'; - -export interface IlmUrlGeneratorState { - page: 'policies_list' | 'policy_edit' | 'policy_create'; - policyName?: string; - absolute?: boolean; -} -export const createIlmUrlGenerator = ( - getAppBasePath: (absolute?: boolean) => Promise -): UrlGeneratorsDefinition => { - return { - id: ILM_URL_GENERATOR_ID, - createUrl: async (state: IlmUrlGeneratorState): Promise => { - switch (state.page) { - case 'policy_create': { - return `${await getAppBasePath(!!state.absolute)}${getPolicyCreatePath()}`; - } - case 'policy_edit': { - return `${await getAppBasePath(!!state.absolute)}${getPolicyEditPath(state.policyName!)}`; - } - case 'policies_list': { - return `${await getAppBasePath(!!state.absolute)}${getPoliciesListPath()}`; - } - } - }, - }; -}; - -export const registerUrlGenerator = ( - coreSetup: CoreSetup, - management: SetupDependencies['management'], - share: SetupDependencies['share'] -) => { - const getAppBasePath = async (absolute = false) => { - const [coreStart] = await coreSetup.getStartServices(); - return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { - path: management.sections.section.data.getApp(PLUGIN.ID)!.basePath, - absolute, - }); - }; - - share.urlGenerators.registerUrlGenerator(createIlmUrlGenerator(getAppBasePath)); -}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index 93cd772ce6658..8e114b0596948 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -22,6 +22,21 @@ import { const nonBreakingSpace = ' '; +const urlServiceMock = { + locators: { + get: () => ({ + getLocation: async () => ({ + app: '', + path: '', + state: {}, + }), + getUrl: async ({ policyName }: { policyName: string }) => `/test/${policyName}`, + navigate: async () => {}, + useUrl: () => '', + }), + }, +}; + describe('Data Streams tab', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: DataStreamsTabTestBed; @@ -38,7 +53,9 @@ describe('Data Streams tab', () => { }); test('displays an empty prompt', async () => { - testBed = await setup(); + testBed = await setup({ + url: urlServiceMock, + }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -54,6 +71,7 @@ describe('Data Streams tab', () => { test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => { testBed = await setup({ plugins: {}, + url: urlServiceMock, }); await act(async () => { @@ -73,6 +91,7 @@ describe('Data Streams tab', () => { test('when Fleet is enabled, links to Fleet', async () => { testBed = await setup({ plugins: { isFleetEnabled: true }, + url: urlServiceMock, }); await act(async () => { @@ -95,6 +114,7 @@ describe('Data Streams tab', () => { testBed = await setup({ plugins: {}, + url: urlServiceMock, }); await act(async () => { @@ -345,6 +365,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -370,13 +391,8 @@ describe('Data Streams tab', () => { }); }); - describe('url generators', () => { - const mockIlmUrlGenerator = { - getUrlGenerator: () => ({ - createUrl: ({ policyName }: { policyName: string }) => `/test/${policyName}`, - }), - }; - test('with an ILM url generator and an ILM policy', async () => { + describe('url locators', () => { + test('with an ILM url locator and an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ @@ -388,7 +404,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: mockIlmUrlGenerator, + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -400,7 +416,7 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyLink().prop('href')).toBe('/test/my_ilm_policy'); }); - test('with an ILM url generator and no ILM policy', async () => { + test('with an ILM url locator and no ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ name: 'dataStream1' }); @@ -409,7 +425,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: mockIlmUrlGenerator, + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -422,7 +438,7 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyName().contains('None')).toBeTruthy(); }); - test('without an ILM url generator and with an ILM policy', async () => { + test('without an ILM url locator and with an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; const dataStreamForDetailPanel = createDataStreamPayload({ @@ -434,7 +450,11 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), - urlGenerators: { getUrlGenerator: () => {} }, + url: { + locators: { + get: () => undefined, + }, + }, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -463,6 +483,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -506,6 +527,7 @@ describe('Data Streams tab', () => { testBed = await setup({ history: createMemoryHistory(), + url: urlServiceMock, }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -542,7 +564,7 @@ describe('Data Streams tab', () => { beforeEach(async () => { setLoadDataStreamsResponse([dataStreamWithDelete, dataStreamNoDelete]); - testBed = await setup({ history: createMemoryHistory() }); + testBed = await setup({ history: createMemoryHistory(), url: urlServiceMock }); await act(async () => { testBed.actions.goToDataStreamsList(); }); diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 3b06d76cf7c26..f8ebfdf7c46b7 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -35,7 +35,7 @@ export interface AppDependencies { history: ScopedHistory; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; uiSettings: CoreSetup['uiSettings']; - urlGenerators: SharePluginStart['urlGenerators']; + url: SharePluginStart['url']; docLinks: CoreStart['docLinks']; } diff --git a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts similarity index 83% rename from x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts rename to x-pack/plugins/index_management/public/application/constants/ilm_locator.ts index ea6cf1756b73c..3da13727af8de 100644 --- a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts +++ b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts @@ -5,5 +5,5 @@ * 2.0. */ -export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID'; +export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; export const ILM_PAGES_POLICY_EDIT = 'policy_edit'; diff --git a/x-pack/plugins/index_management/public/application/constants/index.ts b/x-pack/plugins/index_management/public/application/constants/index.ts index 3bf30517c1145..7a1caf5e50771 100644 --- a/x-pack/plugins/index_management/public/application/constants/index.ts +++ b/x-pack/plugins/index_management/public/application/constants/index.ts @@ -17,4 +17,4 @@ export { export const REACT_ROOT_ID = 'indexManagementReactRoot'; -export * from './ilm_url_generator'; +export * from './ilm_locator'; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 074334ed87725..083a8831291dd 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -62,7 +62,7 @@ export async function mountManagementSection( uiSettings, } = core; - const { urlGenerators } = startDependencies.share; + const { url } = startDependencies.share; docTitle.change(PLUGIN.getI18nName(i18n)); breadcrumbService.setup(setBreadcrumbs); @@ -86,7 +86,7 @@ export async function mountManagementSection( history, setBreadcrumbs, uiSettings, - urlGenerators, + url, docLinks, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 773ccd91a5fb1..a9258c6a3b10b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -29,11 +29,11 @@ import { SectionLoading, SectionError, Error, DataHealth } from '../../../../com import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; -import { useUrlGenerator } from '../../../../services/use_url_generator'; import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing'; -import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../constants'; +import { ILM_PAGES_POLICY_EDIT } from '../../../../constants'; import { useAppContext } from '../../../../app_context'; import { DataStreamsBadges } from '../data_stream_badges'; +import { useIlmLocator } from '../../../../services/use_ilm_locator'; interface DetailsListProps { details: Array<{ @@ -89,13 +89,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ const [isDeleting, setIsDeleting] = useState(false); - const ilmPolicyLink = useUrlGenerator({ - urlGeneratorId: ILM_URL_GENERATOR_ID, - urlGeneratorState: { - page: ILM_PAGES_POLICY_EDIT, - policyName: dataStream?.ilmPolicyName, - }, - }); + const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, dataStream?.ilmPolicyName); const { history } = useAppContext(); let content; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index 2dd2c6e30cfcc..c17ccd9ced932 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -21,8 +21,8 @@ import { EuiSpacer, } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../../../../common'; -import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../../constants'; -import { useUrlGenerator } from '../../../../../services/use_url_generator'; +import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants'; +import { useIlmLocator } from '../../../../../services/use_ilm_locator'; interface Props { templateDetails: TemplateDeserialized; @@ -54,13 +54,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) const numIndexPatterns = indexPatterns.length; - const ilmPolicyLink = useUrlGenerator({ - urlGeneratorId: ILM_URL_GENERATOR_ID, - urlGeneratorState: { - page: ILM_PAGES_POLICY_EDIT, - policyName: ilmPolicy?.name, - }, - }); + const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, ilmPolicy?.name); return ( <> diff --git a/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts new file mode 100644 index 0000000000000..d60cd1cf8aabf --- /dev/null +++ b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useLocatorUrl } from '../../../../../../src/plugins/share/public'; +import { useAppContext } from '../app_context'; +import { ILM_LOCATOR_ID } from '../constants'; + +export const useIlmLocator = ( + page: 'policies_list' | 'policy_edit' | 'policy_create', + policyName?: string +): string => { + const ctx = useAppContext(); + const locator = policyName === undefined ? null : ctx.url.locators.get(ILM_LOCATOR_ID)!; + const url = useLocatorUrl(locator, { page, policyName }, {}, [page, policyName]); + + return url; +}; diff --git a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts b/x-pack/plugins/index_management/public/application/services/use_url_generator.ts deleted file mode 100644 index 2d9ab3959d769..0000000000000 --- a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts +++ /dev/null @@ -1,40 +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 { useEffect, useState } from 'react'; -import { - UrlGeneratorContract, - UrlGeneratorId, - UrlGeneratorStateMapping, -} from '../../../../../../src/plugins/share/public'; -import { useAppContext } from '../app_context'; - -export const useUrlGenerator = ({ - urlGeneratorId, - urlGeneratorState, -}: { - urlGeneratorId: UrlGeneratorId; - urlGeneratorState: UrlGeneratorStateMapping[UrlGeneratorId]['State']; -}) => { - const { urlGenerators } = useAppContext(); - const [link, setLink] = useState(); - useEffect(() => { - const updateLink = async (): Promise => { - let urlGenerator: UrlGeneratorContract; - try { - urlGenerator = urlGenerators.getUrlGenerator(urlGeneratorId); - const url = await urlGenerator.createUrl(urlGeneratorState); - setLink(url); - } catch (e) { - // do nothing - } - }; - - updateLink(); - }, [urlGeneratorId, urlGeneratorState, urlGenerators]); - return link; -}; From eaa0bb9d1d1d6079c061451758517a74fcdf8976 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 21 Jun 2021 23:39:21 +0100 Subject: [PATCH 025/118] [SecuritySolution] Move manual test cases to Cypress (#100730) (#102818) * add scenarios 1-3 * add tests for toggle full screen * add tests for timeline pagination * add tests for timeline correlation tab * fix cypress tests * add data-test-subj for timeline tabs content * fix up * fix flaky tests * fix mark as favorite scenario * fix flaky test * fix flaky test * fix flaky test * refactors 'can be marked as favourite' test * fixes test * fixes typecheck issue * refactors the pipe * little fix * mark as favourite refactor * removes code that causes the flakiness * apply the fix for 7.13 branch * fix timeline api call * fix timeline api call * fix timeline api call * fix syntax Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Gloria Hornero Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Gloria Hornero --- .../integration/cases/attach_timeline.spec.ts | 3 +- .../integration/overview/overview.spec.ts | 19 ++++ .../timeline_templates/creation.spec.ts | 31 ++++++- .../integration/timelines/creation.spec.ts | 57 ++++++++++-- .../timelines/flyout_button.spec.ts | 6 +- .../integration/timelines/full_screen.spec.ts | 41 +++++++++ .../integration/timelines/notes_tab.spec.ts | 65 ++++++++++++-- .../integration/timelines/pagination.spec.ts | 59 +++++++++++++ .../integration/timelines/query_tab.spec.ts | 14 ++- .../timelines/row_renderers.spec.ts | 88 +++++++++++++++++++ .../timelines/search_or_filter.spec.ts | 47 +++++++++- .../cypress/screens/overview.ts | 2 + .../cypress/screens/security_header.ts | 2 + .../cypress/screens/timeline.ts | 77 +++++++++++++++- .../cypress/tasks/api_calls/timelines.ts | 23 +++++ .../cypress/tasks/security_main.ts | 9 ++ .../cypress/tasks/timeline.ts | 54 +++++++++++- .../recent_timelines/recent_timelines.tsx | 6 +- .../timelines/components/flyout/index.tsx | 2 +- .../row_renderers_browser/index.tsx | 2 +- .../timelines/components/timeline/index.tsx | 5 +- .../timeline/search_or_filter/helpers.tsx | 2 + .../timeline/tabs_content/index.tsx | 20 ++++- 23 files changed, 595 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts index 7f0016e39ff88..3f3209b52120e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts @@ -19,8 +19,7 @@ import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; import { createCase } from '../../tasks/api_calls/cases'; -// TODO: enable once attach timeline to cases is re-enabled -describe.skip('attach timeline to case', () => { +describe('attach timeline to case', () => { context('without cases created', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts index dc5b247e3ec43..78ee3fdcdcdd5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts @@ -15,6 +15,8 @@ import { OVERVIEW_URL } from '../../urls/navigation'; import overviewFixture from '../../fixtures/overview_search_strategy.json'; import emptyInstance from '../../fixtures/empty_instance.json'; import { cleanKibana } from '../../tasks/common'; +import { createTimeline, favoriteTimeline } from '../../tasks/api_calls/timelines'; +import { timeline } from '../../objects/timeline'; describe('Overview Page', () => { before(() => { @@ -48,4 +50,21 @@ describe('Overview Page', () => { cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible'); }); }); + + describe('Favorite Timelines', () => { + it('should appear on overview page', () => { + createTimeline(timeline) + .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) + .then((timelineId: string) => { + favoriteTimeline({ timelineId, timelineType: 'default' }).then(() => { + cy.stubSearchStrategyApi(overviewFixture, 'overviewNetwork'); + loginAndWaitForPage(OVERVIEW_URL); + cy.get('[data-test-subj="overview-recent-timelines"]').should( + 'contain', + timeline.title + ); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts index a600b5edfd632..e2c1d7eef38c3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts @@ -16,6 +16,7 @@ import { NOTES_TEXT_AREA, PIN_EVENT, TIMELINE_DESCRIPTION, + TIMELINE_FLYOUT_WRAPPER, TIMELINE_QUERY, TIMELINE_TITLE, } from '../../screens/timeline'; @@ -25,34 +26,38 @@ import { TIMELINES_NOTES_COUNT, TIMELINES_FAVORITE, } from '../../screens/timelines'; +import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { addDescriptionToTimeline, addFilter, addNameToTimeline, addNotesToTimeline, + clickingOnCreateTemplateFromTimelineBtn, closeTimeline, createNewTimelineTemplate, + expandEventAction, markAsFavorite, openTimelineTemplateFromSettings, populateTimeline, waitForTimelineChanges, } from '../../tasks/timeline'; -import { openTimeline } from '../../tasks/timelines'; +import { openTimeline, waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { TIMELINES_URL } from '../../urls/navigation'; describe('Timeline Templates', () => { beforeEach(() => { cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + cy.intercept('PATCH', '/api/timeline').as('timeline'); }); it('Creates a timeline template', async () => { - loginAndWaitForPage(OVERVIEW_URL); openTimelineUsingToggle(); createNewTimelineTemplate(); populateTimeline(); @@ -97,4 +102,22 @@ describe('Timeline Templates', () => { cy.get(NOTES).should('have.text', timeline.notes); }); }); + + it('Create template from timeline', () => { + waitForTimelinesPanelToBeLoaded(); + + createTimeline(timeline).then(() => { + expandEventAction(); + clickingOnCreateTemplateFromTimelineBtn(); + cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { + expect(request.body.timeline).to.haveOwnProperty('templateTimelineId'); + expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( + 'expression', + timeline.query + ); + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index b08bae26bf7ed..8a90b67682cb2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -8,32 +8,37 @@ import { timeline } from '../../objects/timeline'; import { - FAVORITE_TIMELINE, LOCKED_ICON, NOTES_TEXT, PIN_EVENT, + SERVER_SIDE_EVENT_COUNT, TIMELINE_FILTER, + TIMELINE_FLYOUT_WRAPPER, TIMELINE_PANEL, + TIMELINE_TAB_CONTENT_EQL, } from '../../screens/timeline'; +import { createTimelineTemplate } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { + addEqlToTimeline, addFilter, addNameAndDescriptionToTimeline, addNotesToTimeline, + clickingOnCreateTimelineFormTemplateBtn, closeTimeline, createNewTimeline, + expandEventAction, goToQueryTab, - markAsFavorite, pinFirstEvent, populateTimeline, - waitForTimelineChanges, } from '../../tasks/timeline'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { OVERVIEW_URL, TIMELINE_TEMPLATES_URL } from '../../urls/navigation'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; describe('Timelines', (): void => { before(() => { @@ -88,10 +93,44 @@ describe('Timelines', (): void => { cy.get(NOTES_TEXT).should('have.text', timeline.notes); }); - it('can be marked as favorite', () => { - markAsFavorite(); - waitForTimelineChanges(); - cy.get(FAVORITE_TIMELINE).should('have.text', 'Remove from favorites'); + it('should update timeline after adding eql', () => { + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + const eql = 'any where process.name == "which"'; + addEqlToTimeline(eql); + + cy.wait('@updateTimeline', { timeout: 10000 }).its('response.statusCode').should('eq', 200); + + cy.get(`${TIMELINE_TAB_CONTENT_EQL} ${SERVER_SIDE_EVENT_COUNT}`) + .invoke('text') + .then(parseInt) + .should('be.gt', 0); + }); + }); +}); + +describe('Create a timeline from a template', () => { + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL); + waitForTimelinesPanelToBeLoaded(); + }); + + it('Should have the same query and open the timeline modal', () => { + createTimelineTemplate(timeline).then(() => { + expandEventAction(); + cy.intercept('/api/timeline').as('timeline'); + + clickingOnCreateTimelineFormTemplateBtn(); + cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { + if (request.body && request.body.timeline) { + expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( + 'expression', + timeline.query + ); + cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); + } + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts index c7ec17d027e80..38c6f41f1049c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts @@ -61,8 +61,10 @@ describe('timeline flyout button', () => { it('the `(+)` button popover menu owns focus', () => { cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.get(CREATE_NEW_TIMELINE).should('have.focus'); - cy.get('body').type('{esc}'); + cy.get(`${CREATE_NEW_TIMELINE}`) + .pipe(($el) => $el.trigger('focus')) + .should('have.focus'); + cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').type('{esc}'); cy.get(CREATE_NEW_TIMELINE).should('not.be.visible'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts new file mode 100644 index 0000000000000..9cd3b22fc2bb4 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts @@ -0,0 +1,41 @@ +/* + * 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 { TIMELINE_HEADER, TIMELINE_TABS } from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { + openTimelineUsingToggle, + enterFullScreenMode, + exitFullScreenMode, +} from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +describe('Toggle full screen', () => { + before(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + }); + + it('Should hide timeline header and tab list area', () => { + enterFullScreenMode(); + + cy.get(TIMELINE_TABS).should('not.exist'); + cy.get(TIMELINE_HEADER).should('not.be.visible'); + }); + + it('Should show timeline header and tab list area', () => { + exitFullScreenMode(); + cy.get(TIMELINE_TABS).should('exist'); + cy.get(TIMELINE_HEADER).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts index 2505930f72f82..24309b8fda084 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts @@ -7,7 +7,13 @@ import { timelineNonValidQuery } from '../../objects/timeline'; -import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline'; +import { + NOTES_AUTHOR, + NOTES_CODE_BLOCK, + NOTES_LINK, + NOTES_TEXT, + NOTES_TEXT_AREA, +} from '../../screens/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -16,6 +22,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { addNotesToTimeline, closeTimeline, + goToNotesTab, openTimelineById, refreshTimelinesUntilTimeLinePresent, } from '../../tasks/timeline'; @@ -23,8 +30,11 @@ import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; import { TIMELINES_URL } from '../../urls/navigation'; +const text = 'elastic'; +const link = 'https://www.elastic.co/'; + describe('Timeline notes tab', () => { - before(() => { + beforeEach(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); @@ -37,19 +47,62 @@ describe('Timeline notes tab', () => { // request responses and indeterminism since on clicks to activates URL's. .then(() => cy.wait(1000)) .then(() => openTimelineById(timelineId)) - .then(() => addNotesToTimeline(timelineNonValidQuery.notes)) + .then(() => goToNotesTab()) ); }); after(() => { closeTimeline(); }); + it('should render mockdown', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_TEXT_AREA).should('exist'); + }); it('should contain notes', () => { - cy.get(NOTES_TEXT).should('have.text', timelineNonValidQuery.notes); + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_TEXT).first().should('have.text', timelineNonValidQuery.notes); }); - it('should render mockdown', () => { - cy.get(NOTES_TEXT_AREA).should('exist'); + it('should be able to render font in bold', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`**bold**`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(`${NOTES_TEXT} strong`).last().should('have.text', `bold`); + }); + + it('should be able to render font in italics', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`_italics_`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(`${NOTES_TEXT} em`).last().should('have.text', `italics`); + }); + + it('should be able to render code blocks', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(`\`code\``); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_CODE_BLOCK).should('exist'); + }); + + it('should render the right author', () => { + cy.intercept('/api/note').as(`updateNote`); + addNotesToTimeline(timelineNonValidQuery.notes); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_AUTHOR).first().should('have.text', text); + }); + + it('should be able to render a link', () => { + cy.intercept('/api/note').as(`updateNote`); + cy.intercept(link).as(`link`); + addNotesToTimeline(`[${text}](${link})`); + cy.wait('@updateNote').its('response.statusCode').should('eq', 200); + cy.get(NOTES_LINK).last().should('have.text', `${text}(opens in a new tab or window)`); + cy.get(NOTES_LINK).last().click(); + cy.wait('@link').its('response.statusCode').should('eq', 200); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts new file mode 100644 index 0000000000000..568fb90568fb3 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts @@ -0,0 +1,59 @@ +/* + * 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 { + TIMELINE_EVENT, + TIMELINE_EVENTS_COUNT_NEXT_PAGE, + TIMELINE_EVENTS_COUNT_PER_PAGE, + TIMELINE_EVENTS_COUNT_PER_PAGE_BTN, + TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION, + TIMELINE_EVENTS_COUNT_PREV_PAGE, +} from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +const defaultPageSize = 25; +describe('Pagination', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + }); + + it(`should have ${defaultPageSize} events in the page by default`, () => { + cy.get(TIMELINE_EVENT).should('have.length', defaultPageSize); + }); + + it(`should select ${defaultPageSize} items per page by default`, () => { + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', defaultPageSize); + }); + + it('should be able to change items count per page with the dropdown', () => { + const itemsPerPage = 100; + cy.intercept('POST', '/internal/bsearch').as('refetch'); + + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_BTN).first().click(); + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION(itemsPerPage)).click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', itemsPerPage); + }); + + it('should be able to go to next / previous page', () => { + cy.intercept('POST', '/internal/bsearch').as('refetch'); + cy.get(TIMELINE_EVENTS_COUNT_NEXT_PAGE).first().click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + + cy.get(TIMELINE_EVENTS_COUNT_PREV_PAGE).first().click(); + cy.wait('@refetch').its('response.statusCode').should('eq', 200); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts index 672e930bc5072..f37a66ac048fb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts @@ -7,7 +7,13 @@ import { timeline } from '../../objects/timeline'; -import { UNLOCKED_ICON, PIN_EVENT, TIMELINE_FILTER, TIMELINE_QUERY } from '../../screens/timeline'; +import { + UNLOCKED_ICON, + PIN_EVENT, + TIMELINE_FILTER, + TIMELINE_QUERY, + NOTE_CARD_CONTENT, +} from '../../screens/timeline'; import { addNoteToTimeline } from '../../tasks/api_calls/notes'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -18,6 +24,7 @@ import { addFilter, closeTimeline, openTimelineById, + persistNoteToFirstEvent, pinFirstEvent, refreshTimelinesUntilTimeLinePresent, } from '../../tasks/timeline'; @@ -45,6 +52,7 @@ describe('Timeline query tab', () => { ) .then(() => openTimelineById(timelineId)) .then(() => pinFirstEvent()) + .then(() => persistNoteToFirstEvent('event note')) .then(() => addFilter(timeline.filter)); }); }); @@ -58,6 +66,10 @@ describe('Timeline query tab', () => { cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query}`); }); + it('should be able to add event note', () => { + cy.get(NOTE_CARD_CONTENT).should('contain', 'event note'); + }); + it('should display timeline filter', () => { cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts new file mode 100644 index 0000000000000..ed9a7db4702d0 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.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 { + TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN, + TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON, + TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX, + TIMELINE_ROW_RENDERERS_SEARCHBOX, + TIMELINE_SHOW_ROW_RENDERERS_GEAR, +} from '../../screens/timeline'; +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL } from '../../urls/navigation'; + +const RowRenderersId = [ + 'alerts', + 'auditd', + 'auditd_file', + 'library', + 'netflow', + 'plain', + 'registry', + 'suricata', + 'system', + 'system_dns', + 'system_endgame_process', + 'system_file', + 'system_fim', + 'system_security_event', + 'system_socket', + 'threat_match', + 'zeek', +]; + +describe('Row renderers', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + cy.get(TIMELINE_SHOW_ROW_RENDERERS_GEAR).first().click({ force: true }); + }); + + afterEach(() => { + cy.get(TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON).click({ force: true }); + }); + + it('Row renderers should be enabled by default', () => { + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('exist'); + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('be.checked'); + }); + + it('Selected renderer can be disabled and enabled', () => { + cy.get(TIMELINE_ROW_RENDERERS_SEARCHBOX).type('flow'); + + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().uncheck(); + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).to.contain('netflow'); + }); + + cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().check(); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).not.to.contain('netflow'); + }); + }); + + it('Selected renderer can be disabled with one click', () => { + cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).click({ force: true }); + + cy.intercept('PATCH', '/api/timeline').as('updateTimeline'); + cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200); + + cy.wait('@updateTimeline').then((interception) => { + expect(interception.request.body.timeline.excludedRowRendererIds).to.eql(RowRenderersId); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts index 48b00f8afd4eb..9d019cf23ebb1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts @@ -5,14 +5,21 @@ * 2.0. */ -import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline'; +import { + ADD_FILTER, + SERVER_SIDE_EVENT_COUNT, + TIMELINE_KQLMODE_FILTER, + TIMELINE_KQLMODE_SEARCH, + TIMELINE_SEARCH_OR_FILTER, +} from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; -import { loginAndWaitForPage } from '../../tasks/login'; +import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { executeTimelineKQL } from '../../tasks/timeline'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; -import { HOSTS_URL } from '../../urls/navigation'; +import { HOSTS_URL, TIMELINES_URL } from '../../urls/navigation'; describe('timeline search or filter KQL bar', () => { beforeEach(() => { @@ -28,3 +35,37 @@ describe('timeline search or filter KQL bar', () => { cy.get(SERVER_SIDE_EVENT_COUNT).should(($count) => expect(+$count.text()).to.be.gt(0)); }); }); + +describe('Update kqlMode for timeline', () => { + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + openTimelineUsingToggle(); + }); + + beforeEach(() => { + cy.intercept('PATCH', '/api/timeline').as('update'); + cy.get(TIMELINE_SEARCH_OR_FILTER) + .pipe(($el) => $el.trigger('click')) + .should('exist'); + }); + + it('should be able to update timeline kqlMode with filter', () => { + cy.get(TIMELINE_KQLMODE_FILTER).click(); + cy.wait('@update').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'filter'); + cy.get(ADD_FILTER).should('exist'); + }); + }); + + it('should be able to update timeline kqlMode with search', () => { + cy.get(TIMELINE_KQLMODE_SEARCH).click(); + cy.wait('@update').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'search'); + cy.get(ADD_FILTER).should('not.exist'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 1c519b21149a8..ce6c5662ecb9e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -145,3 +145,5 @@ export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="empty-page"]'; + +export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timelines"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts index cb8502ef96029..a3d5b714cdb3f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts @@ -24,3 +24,5 @@ export const OVERVIEW = '[data-test-subj="navigation-overview"]'; export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]'; export const TIMELINES = '[data-test-subj="navigation-timelines"]'; + +export const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 88e207fcea339..0a9e5b44feb1f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -58,6 +58,10 @@ export const UNLOCKED_ICON = '[data-test-subj="timeline-date-picker-unlock-butto export const NOTES = '[data-test-subj="note-card-body"]'; +export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]'; + +export const EVENT_NOTE = '[data-test-subj="timeline-notes-button-small"]'; + export const NOTE_BY_NOTE_ID = (noteId: string) => `[data-test-subj="note-preview-${noteId}"] .euiMarkdownFormat`; @@ -69,6 +73,12 @@ export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]'; export const NOTES_TEXT = '.euiMarkdownFormat'; +export const NOTES_CODE_BLOCK = '.euiCodeBlock__code'; + +export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername'; + +export const NOTES_LINK = '[data-test-subj="markdown-link"]'; + export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; @@ -110,6 +120,8 @@ export const PINNED_TAB_EVENTS_BODY = '[data-test-subj="pinned-tab-flyout-body"] export const PINNED_TAB_EVENTS_FOOTER = '[data-test-subj="pinned-tab-flyout-footer"]'; +export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]'; + export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; @@ -118,6 +130,17 @@ export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiPro export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; +export const TIMELINE_COLLAPSED_ITEMS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]'; + +export const TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN = + '[data-test-subj="create-template-from-timeline"]'; + +export const TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN = '[data-test-subj="create-from-template"]'; + +export const TIMELINE_CORRELATION_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; + +export const TIMELINE_CORRELATION_TAB = '[data-test-subj="timelineTabs-eql"]'; + export const IS_DRAGGING_DATA_PROVIDERS = '.is-dragging'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -143,6 +166,19 @@ export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="save-timeline-descri export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]'; +export const TIMELINE_EVENT = '[data-test-subj="event"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE = '[data-test-subj="local-events-count"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE_BTN = '[data-test-subj="local-events-count-button"]'; + +export const TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION = (itemsPerPage: number) => + `[data-test-subj="items-per-page-option-${itemsPerPage}"]`; + +export const TIMELINE_EVENTS_COUNT_NEXT_PAGE = '[data-test-subj="pagination-button-next"]'; + +export const TIMELINE_EVENTS_COUNT_PREV_PAGE = '[data-test-subj="pagination-button-previous"]'; + export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; @@ -164,6 +200,8 @@ export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header" export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]'; +export const TIMELINE_HEADER = '[data-test-subj="timeline-hide-show-container"]'; + export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`; export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`; @@ -172,6 +210,14 @@ export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle"]'; +export const TIMELINE_SEARCH_OR_FILTER = '[data-test-subj="timeline-select-search-or-filter"]'; + +export const TIMELINE_SEARCH_OR_FILTER_CONTENT = '.searchOrFilterPopover'; + +export const TIMELINE_KQLMODE_SEARCH = '[data-test-subj="kqlModePopoverSearch"]'; + +export const TIMELINE_KQLMODE_FILTER = '[data-test-subj="kqlModePopoverFilter"]'; + export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-title"]'; @@ -186,4 +232,33 @@ export const TIMELINE_EDIT_MODAL_OPEN_BUTTON = '[data-test-subj="save-timeline-b export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]'; -export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]'; +export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]'; + +export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane-wrapper"]'; + +export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]'; + +export const TIMELINE_ROW_RENDERERS_MODAL = '[data-test-subj="row-renderers-modal"]'; + +export const TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN = `[data-test-subj="disable-all"]`; + +export const TIMELINE_ROW_RENDERERS_ENABLE_ALL_BTN = `button[data-test-subj="enable-alll"]`; + +export const TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON = `${TIMELINE_ROW_RENDERERS_MODAL} .euiModal__closeIcon`; + +export const TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX = `${TIMELINE_ROW_RENDERERS_MODAL} .euiCheckbox__input`; + +export const TIMELINE_ROW_RENDERERS_SEARCHBOX = `${TIMELINE_ROW_RENDERERS_MODAL} input[type="search"]`; + +export const TIMELINE_SHOW_ROW_RENDERERS_GEAR = '[data-test-subj="show-row-renderers-gear"]'; + +export const TIMELINE_TABS = '[data-test-subj="timeline"] .euiTabs'; + +export const TIMELINE_TAB_CONTENT_EQL = '[data-test-subj="timeline-tab-content-eql"]'; + +export const TIMELINE_TAB_CONTENT_QUERY = '[data-test-subj="timeline-tab-content-query"]'; + +export const TIMELINE_TAB_CONTENT_PINNED = '[data-test-subj="timeline-tab-content-pinned"]'; + +export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES = + '[data-test-subj="timeline-tab-content-graph-notes"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts index 18359574633e9..8274d19f77a25 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts @@ -119,3 +119,26 @@ export const loadPrepackagedTimelineTemplates = () => url: 'api/timeline/_prepackaged', headers: { 'kbn-xsrf': 'cypress-creds' }, }); + +export const favoriteTimeline = ({ + timelineId, + timelineType, + templateTimelineId, + templateTimelineVersion, +}: { + timelineId: string; + timelineType: string; + templateTimelineId?: string; + templateTimelineVersion?: number; +}) => + cy.request({ + method: 'PATCH', + url: 'api/timeline/_favorite', + body: { + timelineId, + timelineType, + templateTimelineId: templateTimelineId || null, + templateTimelineVersion: templateTimelineVersion || null, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts index 189ef1e46e4bc..01651b7b943d0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -11,6 +11,7 @@ import { TIMELINE_TOGGLE_BUTTON, TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON, } from '../screens/security_main'; +import { TIMELINE_EXIT_FULL_SCREEN_BUTTON, TIMELINE_FULL_SCREEN_BUTTON } from '../screens/timeline'; export const openTimelineUsingToggle = () => { cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).click(); @@ -30,3 +31,11 @@ export const openTimelineIfClosed = () => openTimelineUsingToggle(); } }); + +export const enterFullScreenMode = () => { + cy.get(TIMELINE_FULL_SCREEN_BUTTON).first().click({ force: true }); +}; + +export const exitFullScreenMode = () => { + cy.get(TIMELINE_EXIT_FULL_SCREEN_BUTTON).first().click({ force: true }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 587e4ec45b8c7..af7a7bb5d4c71 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -8,6 +8,7 @@ import { Timeline, TimelineFilter } from '../objects/timeline'; import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases'; +import { LOADING_INDICATOR } from '../screens/security_header'; import { ADD_FILTER, @@ -56,6 +57,13 @@ import { TIMELINE_DATA_PROVIDER_OPERATOR, TIMELINE_DATA_PROVIDER_VALUE, SAVE_DATA_PROVIDER_BTN, + EVENT_NOTE, + TIMELINE_CORRELATION_INPUT, + TIMELINE_CORRELATION_TAB, + TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN, + TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN, + TIMELINE_COLLAPSED_ITEMS_BTN, + TIMELINE_TAB_CONTENT_EQL, } from '../screens/timeline'; import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines'; @@ -99,6 +107,16 @@ export const goToNotesTab = (): Cypress.Chainable> => { return cy.root().find(NOTES_TAB_BUTTON); }; +export const goToCorrelationTab = () => { + cy.root() + .pipe(($el) => { + $el.find(TIMELINE_CORRELATION_TAB).trigger('click'); + return $el.find(`${TIMELINE_TAB_CONTENT_EQL} ${TIMELINE_CORRELATION_INPUT}`); + }) + .should('be.visible'); + return cy.root().find(TIMELINE_CORRELATION_TAB); +}; + export const getNotePreviewByNoteId = (noteId: string) => { return cy.get(`[data-test-subj="note-preview-${noteId}"]`); }; @@ -127,6 +145,12 @@ export const addNotesToTimeline = (notes: string) => { goToNotesTab(); }; +export const addEqlToTimeline = (eql: string) => { + goToCorrelationTab().then(() => { + cy.get(TIMELINE_CORRELATION_INPUT).type(eql); + }); +}; + export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => { cy.get(ADD_FILTER).click(); cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`); @@ -140,7 +164,8 @@ export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => { cy.get(TIMELINE_ADD_FIELD_BUTTON).click(); - cy.wait(300); + cy.get(TIMELINE_DATA_PROVIDER_VALUE).should('have.focus'); // make sure the focus is ready before start typing + cy.get(TIMELINE_DATA_PROVIDER_FIELD).type(`${filter.field}{downarrow}{enter}`); cy.get(TIMELINE_DATA_PROVIDER_OPERATOR).type(filter.operator); cy.get(COMBO_BOX).contains(filter.operator).click(); @@ -209,8 +234,10 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; -export const markAsFavorite = (): Cypress.Chainable> => { - return cy.get(STAR_ICON).click(); +export const markAsFavorite = () => { + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(STAR_ICON).should('be.visible').pipe(click); + cy.get(LOADING_INDICATOR).should('not.exist'); }; export const openTimelineFieldsBrowser = () => { @@ -249,6 +276,15 @@ export const pinFirstEvent = (): Cypress.Chainable> => { return cy.get(PIN_EVENT).first().click({ force: true }); }; +export const persistNoteToFirstEvent = (notes: string) => { + cy.get(EVENT_NOTE).first().click({ force: true }); + cy.get(NOTES_TEXT_AREA).type(notes); + cy.root().pipe(($el) => { + $el.find(ADD_NOTE_BUTTON).trigger('click'); + return $el.find(NOTES_TAB_BUTTON).find('.euiBadge'); + }); +}; + export const populateTimeline = () => { executeTimelineKQL(hostExistsQuery); cy.get(SERVER_SIDE_EVENT_COUNT).should('not.have.text', '0'); @@ -325,3 +361,15 @@ export const refreshTimelinesUntilTimeLinePresent = ( }) .should('be.visible'); }; + +export const clickingOnCreateTimelineFormTemplateBtn = () => { + cy.get(TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN).click({ force: true }); +}; + +export const clickingOnCreateTemplateFromTimelineBtn = () => { + cy.get(TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN).click({ force: true }); +}; + +export const expandEventAction = () => { + cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).first().click(); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx index ace78cec1a52f..ee12c12536af5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx @@ -45,7 +45,11 @@ const RecentTimelinesItem = React.memo( const render = useCallback( (showHoverContent) => ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index 2602ca3f3cc7c..ec46985450d89 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -124,7 +124,7 @@ const FlyoutComponent: React.FC = ({ timelineId, onAppLeave }) => { <> - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 4dcc799d79111..04237bfa43dc6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -115,7 +115,7 @@ const StatefulRowRenderersBrowserComponent: React.FC {show && ( - + = ({ {i18n.TIMELINE_TEMPLATE} )} - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx index d087b24239a66..9479c3209ad85 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx @@ -65,6 +65,7 @@ export const options = [ ), + 'data-test-subj': 'kqlModePopoverFilter', }, { value: modes.search.mode, @@ -84,6 +85,7 @@ export const options = [ ), + 'data-test-subj': 'kqlModePopoverSearch', }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 76a2ad0960322..adaa5f98c88c4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -146,14 +146,20 @@ const ActiveTimelineTab = memo( */ return ( <> - + - + ( /> {timelineType === TimelineType.default && ( - + ( /> )} - + {isGraphOrNotesTabs && getTab(activeTimelineTab)} From ed920362ed27f2bd7932fcfb6c71ceb28dba8778 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 18:56:35 -0400 Subject: [PATCH 026/118] [App Search] Engines Overview polish pass (#102778) (#102819) * Split up engines vs. meta engines into separate panels - per Davey's feedback from earlier UI passes * DRY out manual header/spacing to reusable DataPanel component + update DataPanel icon typing to not error when passed a custom icon/svg - kudos again to Davey for the component * Typography tweaks - Update DataPanel component to accept a custom titleSize (to maintain previous UI/sizing) - Fix meta engines empty prompt title heading to follow heading levels + tweak sizing to not be larger than panel heading * Set up new license CTA button for upcoming meta engines CTA falls back to a documentation link! so fancy * Update Enterprise Search Overview to use new license button * Add new Meta Engines license upgrade CTA - Reuse some copy from Meta Engines creation view - Reuse DataPanel so visuals stay consistent + it looks similar to CTA on Enterprise Search Overview - Update DataPanel to allow buttons to be responsive + conditionally load spacer between header & children * Improve responsiveness of app when platinum license changes Previously, routes/apps were going off the static data passed from the server which was only initialized once on page load. hasPlatinumLicense however changes dynamically and in real-time, removing the need for a hard page refresh. I could have replaced all `canManageMetaEngine` flags with `isPlatinum && canManageEngines`, but I thought baking license checks into the main ability would be more scalable and potentially open the way to other license-based flags also being dynamic. * [PR feedback] Typos in test names Co-authored-by: Jason Stoltzfus * Fix failing test Missed updating the heading level Co-authored-by: Jason Stoltzfus Co-authored-by: Constance Co-authored-by: Jason Stoltzfus --- .../kea_logic/licensing_logic.mock.ts | 3 + .../applications/app_search/app_logic.test.ts | 6 +- .../applications/app_search/app_logic.ts | 6 +- .../components/data_panel/data_panel.test.tsx | 32 +++- .../components/data_panel/data_panel.tsx | 22 ++- .../empty_meta_engines_state.test.tsx | 2 +- .../components/empty_meta_engines_state.tsx | 5 +- .../engines/{constants.ts => constants.tsx} | 28 +++ .../engines/engines_overview.test.tsx | 54 ++++-- .../components/engines/engines_overview.tsx | 159 ++++++++---------- .../utils/role/get_role_abilities.test.ts | 31 +++- .../utils/role/get_role_abilities.ts | 4 +- .../components/license_callout/constants.ts | 7 - .../license_callout/license_callout.test.tsx | 6 +- .../license_callout/license_callout.tsx | 9 +- .../public/applications/index.tsx | 1 + .../applications/shared/licensing/index.ts | 1 + .../shared/licensing/licensing_logic.test.ts | 12 +- .../shared/licensing/licensing_logic.ts | 7 +- .../licensing/manage_license_button.test.tsx | 42 +++++ .../licensing/manage_license_button.tsx | 41 +++++ 21 files changed, 337 insertions(+), 141 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/{constants.ts => constants.tsx} (63%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts index 2cea6061b63ab..f227928b45821 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts @@ -11,8 +11,11 @@ export const mockLicensingValues = { license: licensingMock.createLicense(), hasPlatinumLicense: false, hasGoldLicense: false, + isTrial: false, + canManageLicense: true, }; jest.mock('../../shared/licensing', () => ({ + ...(jest.requireActual('../../shared/licensing') as object), LicensingLogic: { values: mockLicensingValues }, })); 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 7b08e82a4cf20..f69e3492d26eb 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 @@ -6,7 +6,11 @@ */ import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; -import { LogicMounter } from '../__mocks__/kea_logic'; +import { LogicMounter } from '../__mocks__/kea_logic/logic_mounter.test_helper'; + +jest.mock('../shared/licensing', () => ({ + LicensingLogic: { selectors: { hasPlatinumLicense: () => false } }, +})); import { AppLogic } from './app_logic'; 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 44416b596e6ef..90b37e6a4d4ee 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 @@ -9,6 +9,8 @@ import { kea, MakeLogicType } from 'kea'; import { InitialAppData } from '../../../common/types'; +import { LicensingLogic } from '../shared/licensing'; + import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; @@ -43,8 +45,8 @@ export const AppLogic = kea [selectors.account], - ({ role }) => (role ? getRoleAbilities(role) : {}), + (selectors) => [selectors.account, LicensingLogic.selectors.hasPlatinumLicense], + ({ role }, hasPlatinumLicense) => (role ? getRoleAbilities(role, hasPlatinumLicense) : {}), ], }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx index 8034b72d885da..04f05349217c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiIcon, EuiButton } from '@elastic/eui'; +import { EuiIcon, EuiButton, EuiTitle, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { LoadingOverlay } from '../../../shared/loading'; @@ -27,6 +27,16 @@ describe('DataPanel', () => { expect(wrapper.find('[data-test-subj="children"]').text()).toEqual('Look at this graph'); }); + it('conditionally renders a spacer between the header and children', () => { + const wrapper = shallow(Test} />); + + expect(wrapper.find(EuiSpacer)).toHaveLength(0); + + wrapper.setProps({ children: 'hello world' }); + + expect(wrapper.find(EuiSpacer)).toHaveLength(1); + }); + describe('components', () => { it('renders with an icon', () => { const wrapper = shallow(The Smoke Monster} iconType="eye" />); @@ -70,6 +80,26 @@ describe('DataPanel', () => { }); describe('props', () => { + it('passes titleSize to the title', () => { + const wrapper = shallow(Test} />); + + expect(wrapper.find(EuiTitle).prop('size')).toEqual('xs'); // Default + + wrapper.setProps({ titleSize: 's' }); + + expect(wrapper.find(EuiTitle).prop('size')).toEqual('s'); + }); + + it('passes responsive to the header flex group', () => { + const wrapper = shallow(Test} />); + + expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(false); + + wrapper.setProps({ responsive: true }); + + expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(true); + }); + it('renders panel color based on filled flag', () => { const wrapper = shallow(Test} />); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx index ce878dc3cf29a..4b22fbc93d411 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx @@ -13,10 +13,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiIconProps, EuiPanel, EuiSpacer, EuiText, EuiTitle, + EuiTitleProps, } from '@elastic/eui'; import { LoadingOverlay } from '../../../shared/loading'; @@ -25,9 +27,11 @@ import './data_panel.scss'; interface Props { title: React.ReactElement; // e.g., h2 tag - subtitle?: string; - iconType?: string; + titleSize?: EuiTitleProps['size']; + subtitle?: React.ReactNode; + iconType?: EuiIconProps['type']; action?: React.ReactNode; + responsive?: boolean; filled?: boolean; hasBorder?: boolean; isLoading?: boolean; @@ -36,9 +40,11 @@ interface Props { export const DataPanel: React.FC = ({ title, + titleSize = 'xs', subtitle, iconType, action, + responsive = false, filled, hasBorder, isLoading, @@ -59,7 +65,7 @@ export const DataPanel: React.FC = ({ hasShadow={false} aria-busy={isLoading} > - + {iconType && ( @@ -68,7 +74,7 @@ export const DataPanel: React.FC = ({ )} - {title} + {title} {subtitle && ( @@ -79,8 +85,12 @@ export const DataPanel: React.FC = ({ {action && {action}} - - {children} + {children && ( + <> + + {children} + + )} {isLoading && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx index 1eab32d64b77f..8b4f5a69b8141 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx @@ -19,7 +19,7 @@ describe('EmptyMetaEnginesState', () => { .find(EuiEmptyPrompt) .dive(); - expect(wrapper.find('h2').text()).toEqual('Create your first meta engine'); + expect(wrapper.find('h3').text()).toEqual('Create your first meta engine'); expect(wrapper.find(EuiButton).prop('href')).toEqual( expect.stringContaining('/meta-engines-guide.html') ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx index 58bf3f0a0195e..ad96f21022f2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx @@ -15,12 +15,13 @@ import { DOCS_PREFIX } from '../../../routes'; export const EmptyMetaEnginesState: React.FC = () => ( +

{i18n.translate('xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptTitle', { defaultMessage: 'Create your first meta engine', })} -

+ } + titleSize="s" body={

{i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx similarity index 63% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx index d01e89e004d28..223c33f9b9592 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx @@ -5,7 +5,17 @@ * 2.0. */ +import React from 'react'; + +import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DOCS_PREFIX } from '../../routes'; +import { + META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION, + META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK, +} from '../meta_engine_creation/constants'; export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engines.title', { defaultMessage: 'Engines', @@ -21,6 +31,24 @@ export const META_ENGINES_TITLE = i18n.translate( { defaultMessage: 'Meta Engines' } ); +export const META_ENGINES_DESCRIPTION = ( + <> + {META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION} +
+ + {META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK} + + ), + }} + /> + +); + export const SOURCE_ENGINES_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.enginesOverview.metaEnginesTable.sourceEngines.title', { defaultMessage: 'Source Engines' } 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 8825c322fb8d5..a90e1369593d9 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 @@ -42,7 +42,7 @@ describe('EnginesOverview', () => { metaEnginesLoading: false, hasPlatinumLicense: false, // AppLogic - myRole: { canManageEngines: false }, + myRole: { canManageEngines: false, canManageMetaEngines: false }, // MetaEnginesTableLogic expandedSourceEngines: {}, conflictingEnginesSets: {}, @@ -85,17 +85,25 @@ describe('EnginesOverview', () => { expect(actions.loadEngines).toHaveBeenCalled(); }); - describe('when the user can manage/create engines', () => { - it('renders a create engine button which takes users to the create engine page', () => { + describe('engine creation', () => { + it('renders a create engine action when the users can create engines', () => { setMockValues({ ...valuesWithEngines, myRole: { canManageEngines: true }, }); const wrapper = shallow(); - expect( - wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to') - ).toEqual('/engine_creation'); + expect(wrapper.find('[data-test-subj="appSearchEngines"]').prop('action')).toBeTruthy(); + }); + + it('does not render a create engine action if the user cannot create engines', () => { + setMockValues({ + ...valuesWithEngines, + myRole: { canManageEngines: false }, + }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="appSearchEngines"]').prop('action')).toBeFalsy(); }); }); @@ -111,19 +119,41 @@ describe('EnginesOverview', () => { expect(actions.loadMetaEngines).toHaveBeenCalled(); }); - describe('when the user can manage/create engines', () => { - it('renders a create engine button which takes users to the create meta engine page', () => { + describe('meta engine creation', () => { + it('renders a create meta engine action when the user can create meta engines', () => { setMockValues({ ...valuesWithEngines, hasPlatinumLicense: true, - myRole: { canManageEngines: true }, + myRole: { canManageMetaEngines: true }, }); const wrapper = shallow(); - expect( - wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to') - ).toEqual('/meta_engine_creation'); + expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]').prop('action')).toBeTruthy(); }); + + it('does not render a create meta engine action if user cannot create meta engines', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: true, + myRole: { canManageMetaEngines: false }, + }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]').prop('action')).toBeFalsy(); + }); + }); + }); + + describe('when an account does not have a platinum license', () => { + it('renders a license call to action in place of the meta engines table', () => { + setMockValues({ + ...valuesWithEngines, + hasPlatinumLicense: false, + }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="metaEnginesLicenseCTA"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]')).toHaveLength(0); }); }); 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 44111a5ecbe66..4dff246052138 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 @@ -9,23 +9,15 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiPageContentBody, - EuiTitle, - EuiSpacer, -} from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; -import { LicensingLogic } from '../../../shared/licensing'; +import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination'; import { AppLogic } from '../../app_logic'; import { EngineIcon, MetaEngineIcon } from '../../icons'; import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes'; +import { DataPanel } from '../data_panel'; import { AppSearchPageTemplate } from '../layout'; import { LaunchAppSearchButton, EmptyState, EmptyMetaEnginesState } from './components'; @@ -37,13 +29,14 @@ import { CREATE_A_META_ENGINE_BUTTON_LABEL, ENGINES_TITLE, META_ENGINES_TITLE, + META_ENGINES_DESCRIPTION, } from './constants'; import { EnginesLogic } from './engines_logic'; export const EnginesOverview: React.FC = () => { const { hasPlatinumLicense } = useValues(LicensingLogic); const { - myRole: { canManageEngines }, + myRole: { canManageEngines, canManageMetaEngines }, } = useValues(AppLogic); const { @@ -80,93 +73,81 @@ export const EnginesOverview: React.FC = () => { isEmptyState={!engines.length} emptyState={} > - - - - - - - - - -

{ENGINES_TITLE}

- -
- - - - {canManageEngines && ( + {ENGINES_TITLE}} + titleSize="s" + action={ + canManageEngines && ( + + {CREATE_AN_ENGINE_BUTTON_LABEL} + + ) + } + data-test-subj="appSearchEngines" + > + + + + {hasPlatinumLicense ? ( + {META_ENGINES_TITLE}} + titleSize="s" + action={ + canManageMetaEngines && ( - {CREATE_AN_ENGINE_BUTTON_LABEL} + {CREATE_A_META_ENGINE_BUTTON_LABEL} - )} - - - - - + } + onChange={handlePageChange(onMetaEnginesPagination)} /> - - - {hasPlatinumLicense && ( - <> - - - - - - - - - -

{META_ENGINES_TITLE}

-
-
-
-
- - {canManageEngines && ( - - {CREATE_A_META_ENGINE_BUTTON_LABEL} - - )} - -
- - - } - onChange={handlePageChange(onMetaEnginesPagination)} - /> - - - )} - + + ) : ( + {META_ENGINES_TITLE}} + titleSize="s" + subtitle={META_ENGINES_DESCRIPTION} + action={} + data-test-subj="metaEnginesLicenseCTA" + /> + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts index 4d4c84e4146ef..60d0dcc0c5911 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts @@ -10,7 +10,7 @@ import { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__'; import { getRoleAbilities } from './'; describe('getRoleAbilities', () => { - const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role; + const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role as any; it('transforms server role data into a flat role obj with helper shorthands', () => { expect(getRoleAbilities(mockRole)).toEqual({ @@ -53,9 +53,10 @@ describe('getRoleAbilities', () => { describe('can()', () => { it('sets view abilities to true if manage abilities are true', () => { - const role = { ...mockRole }; - role.ability.view = []; - role.ability.manage = ['account_settings']; + const role = { + ...mockRole, + ability: { view: [], manage: ['account_settings'] }, + }; const myRole = getRoleAbilities(role); @@ -70,4 +71,26 @@ describe('getRoleAbilities', () => { expect(myRole.can('edit', 'fakeSubject')).toEqual(false); }); }); + + describe('canManageMetaEngines', () => { + const canManageEngines = { ability: { manage: ['account_engines'] } }; + + it('returns true when the user can manage any engines and the account has a platinum license', () => { + const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, true); + + expect(myRole.canManageMetaEngines).toEqual(true); + }); + + it('returns false when the user can manage any engines but the account does not have a platinum license', () => { + const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, false); + + expect(myRole.canManageMetaEngines).toEqual(false); + }); + + it('returns false when has a platinum license but the user cannot manage any engines', () => { + const myRole = getRoleAbilities({ ...mockRole, ability: { manage: [] } }, true); + + expect(myRole.canManageMetaEngines).toEqual(false); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts index 81ac971d00d44..ef3e22d851f38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts @@ -13,7 +13,7 @@ import { RoleTypes, AbilityTypes, Role } from './types'; * Transforms the `role` data we receive from the Enterprise Search * server into a more convenient format for front-end use */ -export const getRoleAbilities = (role: Account['role']): Role => { +export const getRoleAbilities = (role: Account['role'], hasPlatinumLicense = false): Role => { // Role ability function helpers const myRole = { can: (action: AbilityTypes, subject: string): boolean => { @@ -49,7 +49,7 @@ export const getRoleAbilities = (role: Account['role']): Role => { canViewSettings: myRole.can('view', 'account_settings'), canViewRoleMappings: myRole.can('view', 'role_mappings'), canManageEngines: myRole.can('manage', 'account_engines'), - canManageMetaEngines: myRole.can('manage', 'account_meta_engines'), + canManageMetaEngines: hasPlatinumLicense && myRole.can('manage', 'account_engines'), canManageLogSettings: myRole.can('manage', 'account_log_settings'), canManageSettings: myRole.can('manage', 'account_settings'), canManageEngineCrawler: myRole.can('manage', 'engine_crawler'), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts index 903d1768f3cc1..f51eeb1c8160c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts @@ -11,10 +11,3 @@ export const LICENSE_CALLOUT_BODY = i18n.translate('xpack.enterpriseSearch.licen defaultMessage: 'Enterprise authentication via SAML, document-level permission and authorization support, custom search experiences and more are available with a valid Platinum license.', }); - -export const LICENSE_CALLOUT_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.licenseCalloutButton', - { - defaultMessage: 'Manage your license', - } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx index 0c77a0fbf6f5a..75a9700691ebb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx @@ -13,7 +13,7 @@ import { shallow } from 'enzyme'; import { EuiPanel, EuiText } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { ManageLicenseButton } from '../../../shared/licensing'; import { LicenseCallout } from './'; @@ -27,9 +27,7 @@ describe('LicenseCallout', () => { expect(wrapper.find(EuiPanel)).toHaveLength(1); expect(wrapper.find(EuiText)).toHaveLength(2); - expect(wrapper.find(EuiButtonTo).prop('to')).toEqual( - '/app/management/stack/license_management' - ); + expect(wrapper.find(ManageLicenseButton)).toHaveLength(1); }); it('does not render for platinum', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx index 4a4de17450f1b..f9f329c859110 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx @@ -11,12 +11,11 @@ import { useValues } from 'kea'; import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { LicensingLogic } from '../../../shared/licensing'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing'; import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants'; -import { LICENSE_CALLOUT_BODY, LICENSE_CALLOUT_BUTTON } from './constants'; +import { LICENSE_CALLOUT_BODY } from './constants'; export const LicenseCallout: React.FC = () => { const { hasPlatinumLicense, isTrial } = useValues(LicensingLogic); @@ -34,9 +33,7 @@ export const LicenseCallout: React.FC = () => { - - {LICENSE_CALLOUT_BUTTON} - + diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index ba2b28e64b9cf..414957656467a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -57,6 +57,7 @@ export const renderApp = ( }); const unmountLicensingLogic = mountLicensingLogic({ license$: plugins.licensing.license$, + canManageLicense: core.application.capabilities.management?.stack?.license_management, }); const unmountHttpLogic = mountHttpLogic({ http: core.http, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts index c83e578bdd090..74281d45ae0a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -6,3 +6,4 @@ */ export { LicensingLogic, mountLicensingLogic } from './licensing_logic'; +export { ManageLicenseButton } from './manage_license_button'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts index 4ea74e1c0d4f2..5d210cee1a926 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts @@ -15,13 +15,21 @@ import { LicensingLogic, mountLicensingLogic } from './licensing_logic'; describe('LicensingLogic', () => { const mockLicense = licensingMock.createLicense(); const mockLicense$ = new BehaviorSubject(mockLicense); - const mount = () => mountLicensingLogic({ license$: mockLicense$ }); + const mount = (props?: object) => + mountLicensingLogic({ license$: mockLicense$, canManageLicense: true, ...props }); beforeEach(() => { jest.clearAllMocks(); resetContext({}); }); + describe('canManageLicense', () => { + it('sets value from props', () => { + mount({ canManageLicense: false }); + expect(LicensingLogic.values.canManageLicense).toEqual(false); + }); + }); + describe('setLicense()', () => { it('sets license value', () => { mount(); @@ -61,7 +69,7 @@ describe('LicensingLogic', () => { describe('on unmount', () => { it('unsubscribes to the license observable', () => { const mockUnsubscribe = jest.fn(); - const unmount = mountLicensingLogic({ + const unmount = mount({ license$: { subscribe: () => ({ unsubscribe: mockUnsubscribe }) } as any, }); unmount(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts index 7d0222f476214..f94a1fff0cd31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts @@ -16,6 +16,7 @@ interface LicensingValues { hasPlatinumLicense: boolean; hasGoldLicense: boolean; isTrial: boolean; + canManageLicense: boolean; } interface LicensingActions { setLicense(license: ILicense): ILicense; @@ -28,7 +29,7 @@ export const LicensingLogic = kea license, setLicenseSubscription: (licenseSubscription) => licenseSubscription, }, - reducers: { + reducers: ({ props }) => ({ license: [ null, { @@ -41,7 +42,8 @@ export const LicensingLogic = kea licenseSubscription, }, ], - }, + canManageLicense: [props.canManageLicense || false, {}], + }), selectors: { hasPlatinumLicense: [ (selectors) => [selectors.license], @@ -80,6 +82,7 @@ export const LicensingLogic = kea; + canManageLicense: boolean; } export const mountLicensingLogic = (props: LicensingLogicProps) => { LicensingLogic(props); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx new file mode 100644 index 0000000000000..1877a4cbd0e42 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { EuiButtonTo } from '../react_router_helpers'; + +import { ManageLicenseButton } from './'; + +describe('ManageLicenseButton', () => { + describe('when the user can access license management', () => { + it('renders a SPA link to the license management plugin', () => { + setMockValues({ canManageLicense: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual( + '/app/management/stack/license_management' + ); + }); + }); + + describe('when the user cannot access license management', () => { + it('renders an external link to our license management documentation', () => { + setMockValues({ canManageLicense: false }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/license-management.html') + ); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx new file mode 100644 index 0000000000000..af3b33e3d7a3d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx @@ -0,0 +1,41 @@ +/* + * 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 { useValues } from 'kea'; + +import { EuiButton, EuiButtonProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { docLinks } from '../doc_links'; +import { EuiButtonTo } from '../react_router_helpers'; + +import { LicensingLogic } from './licensing_logic'; + +export const ManageLicenseButton: React.FC = (props) => { + const { canManageLicense } = useValues(LicensingLogic); + + return canManageLicense ? ( + + {i18n.translate('xpack.enterpriseSearch.licenseManagementLink', { + defaultMessage: 'Manage your license', + })} + + ) : ( + + {i18n.translate('xpack.enterpriseSearch.licenseDocumentationLink', { + defaultMessage: 'Learn more about license features', + })} + + ); +}; From ddb9688f7b1fa62a92938fa9974cff87551c0be9 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 21 Jun 2021 16:24:04 -0700 Subject: [PATCH 027/118] [App Search] Convert Settings & Credentials pages to new page template (#102671) (#102823) * Convert Settings to new page template + add missing ability check around route * Convert Credentials to new page template + add missing ability check around route * [Tests refactor] DRY out repeated ability tests to a helper Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../credentials/credentials.test.tsx | 5 +- .../components/credentials/credentials.tsx | 133 +++++++++--------- .../log_retention/log_retention_panel.tsx | 14 +- .../components/settings/settings.test.tsx | 4 +- .../components/settings/settings.tsx | 20 +-- .../applications/app_search/index.test.tsx | 66 +++------ .../public/applications/app_search/index.tsx | 24 +++- 7 files changed, 124 insertions(+), 142 deletions(-) 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 286658c011002..737908752911d 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 @@ -12,7 +12,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; +import { EuiCopy, EuiLoadingContent } from '@elastic/eui'; import { DEFAULT_META } from '../../../shared/constants'; import { externalUrl } from '../../../shared/enterprise_search_url'; @@ -20,6 +20,7 @@ import { externalUrl } from '../../../shared/enterprise_search_url'; import { Credentials } from './credentials'; import { CredentialsFlyout } from './credentials_flyout'; +import { CredentialsList } from './credentials_list'; describe('Credentials', () => { // Kea mocks @@ -42,7 +43,7 @@ describe('Credentials', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + expect(wrapper.find(CredentialsList)).toHaveLength(1); }); it('fetches data on mount', () => { 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 8918445982ea6..f81d8d64737df 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 @@ -10,9 +10,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { - EuiPageHeader, EuiTitle, - EuiPageContentBody, EuiPanel, EuiCopy, EuiButtonIcon, @@ -25,8 +23,7 @@ import { import { i18n } from '@kbn/i18n'; import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { CREDENTIALS_TITLE } from './constants'; import { CredentialsFlyout } from './credentials_flyout'; @@ -52,74 +49,72 @@ export const Credentials: React.FC = () => { }, []); return ( - <> - - - - {shouldShowCredentialsForm && } - - + + {shouldShowCredentialsForm && } + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', { + defaultMessage: 'Endpoint', + })} +

+
+ + {(copy) => ( + <> + + {externalUrl.enterpriseSearchUrl} + + )} + +
+ + + +

- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', { - defaultMessage: 'Endpoint', + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { + defaultMessage: 'API Keys', })}

- - {(copy) => ( - <> - - {externalUrl.enterpriseSearchUrl} - - )} - -
- - - - -

- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { - defaultMessage: 'API Keys', - })} -

-
-
- - {!dataLoading && ( - showCredentialsForm()} - > - {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { - defaultMessage: 'Create a key', - })} - - )} - -
- - - - {!!dataLoading ? : } - -
- + + + {!dataLoading && ( + showCredentialsForm()} + > + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { + defaultMessage: 'Create a key', + })} + + )} + + + + + {!!dataLoading ? : } + + ); }; 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 76fdcdac58ad4..fb4b503c7e62c 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 @@ -9,7 +9,15 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; +import { + EuiPanel, + EuiLink, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; @@ -30,7 +38,7 @@ export const LogRetentionPanel: React.FC = () => { }, []); return ( -
+

{i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.title', { @@ -104,6 +112,6 @@ export const LogRetentionPanel: React.FC = () => { data-test-subj="LogRetentionPanelAPISwitch" /> -

+ ); }; 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 41d446b8e36fc..1ad12856a92e1 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 @@ -9,13 +9,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageContentBody } from '@elastic/eui'; +import { LogRetentionPanel } from './log_retention'; import { Settings } from './settings'; describe('Settings', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + expect(wrapper.find(LogRetentionPanel)).toHaveLength(1); }); }); 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 2d5dd08f81288..ddbf046d75ec1 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 @@ -7,10 +7,7 @@ import React from 'react'; -import { EuiPageHeader, EuiPageContent, EuiPageContentBody } from '@elastic/eui'; - -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { AppSearchPageTemplate } from '../layout'; import { LogRetentionPanel, LogRetentionConfirmationModal } from './log_retention'; @@ -18,16 +15,9 @@ import { SETTINGS_TITLE } from './'; export const Settings: React.FC = () => { return ( - <> - - - - - - - - - - + + + + ); }; 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 4d8ff80326715..2402a6ecc6401 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 @@ -24,6 +24,7 @@ import { rerender } from '../test_helpers'; jest.mock('./app_logic', () => ({ AppLogic: jest.fn() })); import { AppLogic } from './app_logic'; +import { Credentials } from './components/credentials'; import { EngineRouter, EngineNav } from './components/engine'; import { EngineCreation } from './components/engine_creation'; import { EnginesOverview } from './components/engines'; @@ -31,6 +32,7 @@ import { ErrorConnecting } from './components/error_connecting'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; import { RoleMappings } from './components/role_mappings'; +import { Settings } from './components/settings'; import { SetupGuide } from './components/setup_guide'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; @@ -103,52 +105,28 @@ describe('AppSearchConfigured', () => { expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); }); - describe('ability checks', () => { - describe('canViewRoleMappings', () => { - it('renders RoleMappings when canViewRoleMappings is true', () => { - setMockValues({ myRole: { canViewRoleMappings: true } }); - rerender(wrapper); - expect(wrapper.find(RoleMappings)).toHaveLength(1); + describe('routes with ability checks', () => { + const runRouteAbilityCheck = (routeAbility: string, View: React.FC) => { + describe(View.name, () => { + it(`renders ${View.name} when user ${routeAbility} is true`, () => { + setMockValues({ myRole: { [routeAbility]: true } }); + rerender(wrapper); + expect(wrapper.find(View)).toHaveLength(1); + }); + + it(`does not render ${View.name} when user ${routeAbility} is false`, () => { + setMockValues({ myRole: { [routeAbility]: false } }); + rerender(wrapper); + expect(wrapper.find(View)).toHaveLength(0); + }); }); + }; - it('does not render RoleMappings when user canViewRoleMappings is false', () => { - setMockValues({ myRole: { canManageEngines: false } }); - rerender(wrapper); - expect(wrapper.find(RoleMappings)).toHaveLength(0); - }); - }); - - describe('canManageEngines', () => { - it('renders EngineCreation when user canManageEngines is true', () => { - setMockValues({ myRole: { canManageEngines: true } }); - rerender(wrapper); - - expect(wrapper.find(EngineCreation)).toHaveLength(1); - }); - - it('does not render EngineCreation when user canManageEngines is false', () => { - setMockValues({ myRole: { canManageEngines: false } }); - rerender(wrapper); - - expect(wrapper.find(EngineCreation)).toHaveLength(0); - }); - }); - - describe('canManageMetaEngines', () => { - it('renders MetaEngineCreation when user canManageMetaEngines is true', () => { - setMockValues({ myRole: { canManageMetaEngines: true } }); - rerender(wrapper); - - expect(wrapper.find(MetaEngineCreation)).toHaveLength(1); - }); - - it('does not render MetaEngineCreation when user canManageMetaEngines is false', () => { - setMockValues({ myRole: { canManageMetaEngines: false } }); - rerender(wrapper); - - expect(wrapper.find(MetaEngineCreation)).toHaveLength(0); - }); - }); + runRouteAbilityCheck('canViewSettings', Settings); + runRouteAbilityCheck('canViewAccountCredentials', Credentials); + runRouteAbilityCheck('canViewRoleMappings', RoleMappings); + runRouteAbilityCheck('canManageEngines', EngineCreation); + runRouteAbilityCheck('canManageMetaEngines', MetaEngineCreation); }); describe('library', () => { 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 d724371cf1dc6..7b3b13aef05d6 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 @@ -76,7 +76,13 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC> = (props) => { const { - myRole: { canManageEngines, canManageMetaEngines, canViewRoleMappings }, + myRole: { + canManageEngines, + canManageMetaEngines, + canViewSettings, + canViewAccountCredentials, + canViewRoleMappings, + }, } = useValues(AppLogic(props)); const { renderHeaderActions } = useValues(KibanaLogic); const { readOnlyMode } = useValues(HttpLogic); @@ -111,6 +117,16 @@ export const AppSearchConfigured: React.FC> = (props) =
)} + {canViewSettings && ( + + + + )} + {canViewAccountCredentials && ( + + + + )} {canViewRoleMappings && ( @@ -119,12 +135,6 @@ export const AppSearchConfigured: React.FC> = (props) = } readOnlyMode={readOnlyMode}> - - - - - - From d6df6595e36088c7e3d197f40893626248520021 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 19:43:38 -0400 Subject: [PATCH 028/118] [App Search] Convert Documents views to new page template + minor UI polish (#102807) (#102824) * Convert Documents view to new page template * [UI polish] Move empty state to top-level instead of showing full UI - per Davey's previous approval * [UX polish] Show loading indicator on initial documents page load * Convert single Document detail view to new page template * Update router Co-authored-by: Constance --- .../documents/components/empty_state.tsx | 66 +++++++++---------- .../documents/document_detail.test.tsx | 21 ++---- .../components/documents/document_detail.tsx | 44 +++++-------- .../components/documents/documents.test.tsx | 14 ++-- .../components/documents/documents.tsx | 33 +++++----- .../search_experience/search_experience.scss | 1 + .../search_experience.test.tsx | 10 --- .../search_experience/search_experience.tsx | 7 +- .../search_experience_content.test.tsx | 5 +- .../search_experience_content.tsx | 3 +- .../components/engine/engine_router.tsx | 20 +++--- 11 files changed, 90 insertions(+), 134 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx index 0f9455a3b9228..39fe02a84854c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx @@ -7,43 +7,41 @@ import React from 'react'; -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; export const EmptyState = () => ( - - - {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.title', { - defaultMessage: 'Add your first documents', - })} - - } - body={ -

- {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.description', { - defaultMessage: - 'You can index documents using the App Search Web Crawler, by uploading JSON, or by using the API.', - })} -

- } - actions={ - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', { - defaultMessage: 'Read the documents guide', - })} - - } - /> -
+ + {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.title', { + defaultMessage: 'Add your first documents', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.description', { + defaultMessage: + 'You can index documents using the App Search Web Crawler, by uploading JSON, or by using the API.', + })} +

+ } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', { + defaultMessage: 'Read the documents guide', + })} + + } + /> ); 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 4aade8e61b085..90da5bebe6d23 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 @@ -14,9 +14,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader, EuiPageContent, EuiBasicTable } from '@elastic/eui'; +import { EuiPanel, EuiBasicTable } from '@elastic/eui'; + +import { getPageHeaderActions } from '../../../test_helpers'; -import { Loading } from '../../../shared/loading'; import { ResultFieldValue } from '../result'; import { DocumentDetail } from '.'; @@ -45,7 +46,7 @@ describe('DocumentDetail', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageContent).length).toBe(1); + expect(wrapper.find(EuiPanel).length).toBe(1); }); it('initializes data on mount', () => { @@ -59,17 +60,6 @@ describe('DocumentDetail', () => { expect(actions.setFields).toHaveBeenCalledWith([]); }); - it('will show a loader while data is loading', () => { - setMockValues({ - ...values, - dataLoading: true, - }); - - const wrapper = shallow(); - - expect(wrapper.find(Loading).length).toBe(1); - }); - describe('field values list', () => { let columns: any; @@ -102,8 +92,7 @@ describe('DocumentDetail', () => { it('will delete the document when the delete button is pressed', () => { const wrapper = shallow(); - const header = wrapper.find(EuiPageHeader).dive().children().dive(); - const button = header.find('[data-test-subj="DeleteDocumentButton"]'); + const button = getPageHeaderActions(wrapper).find('[data-test-subj="DeleteDocumentButton"]'); button.simulate('click'); 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 314c3529cf4db..175fb1239d380 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 @@ -10,22 +10,13 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { - EuiButton, - EuiPageHeader, - EuiPageContentBody, - EuiPageContent, - EuiBasicTable, - EuiBasicTableColumn, -} from '@elastic/eui'; +import { EuiPanel, EuiButton, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DELETE_BUTTON_LABEL } from '../../../shared/constants'; -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 { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { ResultFieldValue } from '../result'; import { DOCUMENTS_TITLE } from './constants'; @@ -52,10 +43,6 @@ export const DocumentDetail: React.FC = () => { }; }, []); - if (dataLoading) { - return ; - } - const columns: Array> = [ { name: i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.fieldHeader', { @@ -74,11 +61,11 @@ export const DocumentDetail: React.FC = () => { ]; return ( - <> - - { > {DELETE_BUTTON_LABEL} , - ]} - /> - - - - - - - + ], + }} + isLoading={dataLoading} + > + + + + ); }; 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 143ad3f55ff2f..b5b6dd453c9df 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 @@ -10,9 +10,9 @@ import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; +import { getPageHeaderActions } from '../../../test_helpers'; import { DocumentCreationButton } from './components'; import { SearchExperience } from './search_experience'; @@ -22,6 +22,7 @@ import { Documents } from '.'; describe('Documents', () => { const values = { isMetaEngine: false, + engine: { document_count: 1 }, myRole: { canManageEngineDocuments: true }, }; @@ -36,9 +37,6 @@ describe('Documents', () => { }); describe('DocumentCreationButton', () => { - const getHeader = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).dive().children().dive(); - it('renders a DocumentCreationButton if the user can manage engine documents', () => { setMockValues({ ...values, @@ -46,7 +44,7 @@ describe('Documents', () => { }); const wrapper = shallow(); - expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(true); + expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(true); }); it('does not render a DocumentCreationButton if the user cannot manage engine documents', () => { @@ -56,7 +54,7 @@ describe('Documents', () => { }); const wrapper = shallow(); - expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(false); + expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(false); }); it('does not render a DocumentCreationButton for meta engines even if the user can manage engine documents', () => { @@ -67,7 +65,7 @@ describe('Documents', () => { }); const wrapper = shallow(); - expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(false); + expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(false); }); }); 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 b4122a715f927..62c7759757bda 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 @@ -9,35 +9,32 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiPageHeader, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - import { AppLogic } from '../../app_logic'; import { EngineLogic, getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; -import { DocumentCreationButton } from './components'; +import { DocumentCreationButton, EmptyState } from './components'; import { DOCUMENTS_TITLE } from './constants'; import { SearchExperience } from './search_experience'; export const Documents: React.FC = () => { - const { isMetaEngine } = useValues(EngineLogic); + const { isMetaEngine, engine } = useValues(EngineLogic); const { myRole } = useValues(AppLogic); return ( - <> - - ] - : undefined - } - /> - + ] : [], + }} + isEmptyState={!engine.document_count} + emptyState={} + > {isMetaEngine && ( <> { )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss index d2e0a8155fa55..34aac402fbb39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss @@ -15,6 +15,7 @@ .documentsSearchExperience__content { flex-grow: 4; + position: relative; } .documentsSearchExperience__pagingInfo { 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 a4d1a92ee45a4..3e8a9c1ab307c 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 @@ -20,8 +20,6 @@ jest.mock('../../../../shared/use_local_storage', () => ({ })); import { useLocalStorage } from '../../../../shared/use_local_storage'; -import { EmptyState } from '../components'; - import { CustomizationCallout } from './customization_callout'; import { CustomizationModal } from './customization_modal'; import { SearchExperienceContent } from './search_experience_content'; @@ -58,14 +56,6 @@ describe('SearchExperience', () => { expect(wrapper.find(SearchExperienceContent)).toHaveLength(1); }); - it('renders an empty state when the engine does not have documents', () => { - setMockValues({ ...values, engine: { ...values.engine, document_count: 0 } }); - const wrapper = shallow(); - - expect(wrapper.find(EmptyState)).toHaveLength(1); - expect(wrapper.find(SearchExperienceContent)).toHaveLength(0); - }); - describe('when there are no selected filter fields', () => { let wrapper: ShallowWrapper; beforeEach(() => { 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 22029956601a6..709dfc69905f0 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 @@ -21,7 +21,6 @@ import './search_experience.scss'; import { externalUrl } from '../../../../shared/enterprise_search_url'; import { useLocalStorage } from '../../../../shared/use_local_storage'; import { EngineLogic } from '../../engine'; -import { EmptyState } from '../components'; import { buildSearchUIConfig } from './build_search_ui_config'; import { buildSortOptions } from './build_sort_options'; @@ -141,11 +140,7 @@ export const SearchExperience: React.FC = () => { )} - {engine.document_count && engine.document_count > 0 ? ( - - ) : ( - - )} + 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 44a6da51ec8d6..e573502d76b9f 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 @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; // @ts-expect-error types are not available for this package yet import { Results } from '@elastic/react-search-ui'; +import { Loading } from '../../../../shared/loading'; import { SchemaType } from '../../../../shared/schema/types'; import { Pagination } from './pagination'; @@ -82,13 +83,13 @@ describe('SearchExperienceContent', () => { expect(wrapper.find(Pagination).exists()).toBe(true); }); - it('renders empty if a search was not performed yet', () => { + it('renders a loading state if a search was not performed yet', () => { setMockSearchContextState({ ...searchState, wasSearched: false, }); const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(true); + expect(wrapper.find(Loading)).toHaveLength(1); }); it('renders results if a search was performed and there are more than 0 totalResults', () => { 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 84fe721f9eb7f..2322bcde831eb 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 @@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiSpacer, EuiEmptyPrompt } from '@elastic/eui'; import { Results, Paging, ResultsPerPage } from '@elastic/react-search-ui'; import { i18n } from '@kbn/i18n'; +import { Loading } from '../../../../shared/loading'; import { EngineLogic } from '../../engine'; import { Result } from '../../result/types'; @@ -26,7 +27,7 @@ export const SearchExperienceContent: React.FC = () => { const { isMetaEngine, engine } = useValues(EngineLogic); - if (!wasSearched) return null; + if (!wasSearched) return ; if (totalResults) { return ( 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 6510e99a000fc..98627950016fb 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 @@ -94,6 +94,16 @@ export const EngineRouter: React.FC = () => { + {canViewEngineDocuments && ( + + + + )} + {canViewEngineDocuments && ( + + + + )} {/* TODO: Remove layout once page template migration is over */} }> {canViewEngineAnalytics && ( @@ -101,16 +111,6 @@ export const EngineRouter: React.FC = () => {
)} - {canViewEngineDocuments && ( - - - - )} - {canViewEngineDocuments && ( - - - - )} {canViewEngineSchema && ( From ebacdde0fe7cc53792e7025fa38c42ae64b90d32 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 20:40:18 -0400 Subject: [PATCH 029/118] [Security Solution][Exceptions] Fixes empty exceptions filter bug (#102583) (#102830) Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> --- packages/kbn-securitysolution-list-utils/src/helpers/index.ts | 4 ++++ .../public/exceptions/components/builder/helpers.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts index a483da152ac89..d208624b69fc5 100644 --- a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts @@ -95,6 +95,10 @@ export const filterExceptionItems = ( } }, []); + if (entries.length === 0) { + return acc; + } + const item = { ...exception, entries }; if (exceptionListItemSchema.is(item)) { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts index ec46038c397e5..212db40f3168c 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts @@ -1697,9 +1697,9 @@ describe('Exception builder helpers', () => { namespaceType: 'single', ruleName: 'rule name', }); - const exceptions = filterExceptionItems([{ ...rest, meta }]); + const exceptions = filterExceptionItems([{ ...rest, entries: [getEntryMatchMock()], meta }]); - expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); + expect(exceptions).toEqual([{ ...rest, entries: [getEntryMatchMock()], meta: undefined }]); }); }); From 7f155927d789ee7b80a2740f7a1d436117b4f1aa Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 20:41:08 -0400 Subject: [PATCH 030/118] [kbn/test/es] remove unnecessary es user management logic (#102584) (#102831) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Spencer Co-authored-by: spalger --- .../kbn-test/src/functional_tests/lib/auth.ts | 188 ------------------ .../functional_tests/lib/run_elasticsearch.ts | 23 +-- packages/kbn-test/src/index.ts | 2 - src/core/test_helpers/kbn_server.ts | 25 +-- 4 files changed, 3 insertions(+), 235 deletions(-) delete mode 100644 packages/kbn-test/src/functional_tests/lib/auth.ts diff --git a/packages/kbn-test/src/functional_tests/lib/auth.ts b/packages/kbn-test/src/functional_tests/lib/auth.ts deleted file mode 100644 index abd1e0f9e7d5e..0000000000000 --- a/packages/kbn-test/src/functional_tests/lib/auth.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import fs from 'fs'; -import util from 'util'; -import { format as formatUrl } from 'url'; -import request from 'request'; -import type { ToolingLog } from '@kbn/dev-utils'; - -export const DEFAULT_SUPERUSER_PASS = 'changeme'; -const readFile = util.promisify(fs.readFile); - -function delay(delayMs: number) { - return new Promise((res) => setTimeout(res, delayMs)); -} - -interface UpdateCredentialsOptions { - port: number; - auth: string; - username: string; - password: string; - retries?: number; - protocol: string; - caCert?: Buffer | string; -} -async function updateCredentials({ - port, - auth, - username, - password, - retries = 10, - protocol, - caCert, -}: UpdateCredentialsOptions): Promise { - const result = await new Promise<{ body: any; httpResponse: request.Response }>( - (resolve, reject) => - request( - { - method: 'PUT', - uri: formatUrl({ - protocol: `${protocol}:`, - auth, - hostname: 'localhost', - port, - pathname: `/_security/user/${username}/_password`, - }), - json: true, - body: { password }, - ca: caCert, - }, - (err, httpResponse, body) => { - if (err) return reject(err); - resolve({ httpResponse, body }); - } - ) - ); - - const { body, httpResponse } = result; - const { statusCode } = httpResponse; - - if (statusCode === 200) { - return; - } - - if (retries > 0) { - await delay(2500); - return await updateCredentials({ - port, - auth, - username, - password, - retries: retries - 1, - protocol, - caCert, - }); - } - - throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); -} - -interface SetupUsersOptions { - log: ToolingLog; - esPort: number; - updates: Array<{ username: string; password: string; roles?: string[] }>; - protocol?: string; - caPath?: string; -} - -export async function setupUsers({ - log, - esPort, - updates, - protocol = 'http', - caPath, -}: SetupUsersOptions): Promise { - // track the current credentials for the `elastic` user as - // they will likely change as we apply updates - let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`; - const caCert = caPath ? await readFile(caPath) : undefined; - - for (const { username, password, roles } of updates) { - // If working with a built-in user, just change the password - if (['logstash_system', 'elastic', 'kibana'].includes(username)) { - await updateCredentials({ port: esPort, auth, username, password, protocol, caCert }); - log.info('setting %j user password to %j', username, password); - - // If not a builtin user, add them - } else { - await insertUser({ port: esPort, auth, username, password, roles, protocol, caCert }); - log.info('Added %j user with password to %j', username, password); - } - - if (username === 'elastic') { - auth = `elastic:${password}`; - } - } -} - -interface InserUserOptions { - port: number; - auth: string; - username: string; - password: string; - roles?: string[]; - retries?: number; - protocol: string; - caCert?: Buffer | string; -} -async function insertUser({ - port, - auth, - username, - password, - roles = [], - retries = 10, - protocol, - caCert, -}: InserUserOptions): Promise { - const result = await new Promise<{ body: any; httpResponse: request.Response }>( - (resolve, reject) => - request( - { - method: 'POST', - uri: formatUrl({ - protocol: `${protocol}:`, - auth, - hostname: 'localhost', - port, - pathname: `/_security/user/${username}`, - }), - json: true, - body: { password, roles }, - ca: caCert, - }, - (err, httpResponse, body) => { - if (err) return reject(err); - resolve({ httpResponse, body }); - } - ) - ); - - const { body, httpResponse } = result; - const { statusCode } = httpResponse; - if (statusCode === 200) { - return; - } - - if (retries > 0) { - await delay(2500); - return await insertUser({ - port, - auth, - username, - password, - roles, - retries: retries - 1, - protocol, - caCert, - }); - } - - throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); -} diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 7ba9a3c1c4733..da83d8285a6b5 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -12,8 +12,6 @@ import { KIBANA_ROOT } from './paths'; import type { Config } from '../../functional_test_runner/'; import { createTestEsCluster } from '../../es'; -import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth'; - interface RunElasticsearchOptions { log: ToolingLog; esFrom: string; @@ -34,9 +32,7 @@ export async function runElasticsearch({ const cluster = createTestEsCluster({ port: config.get('servers.elasticsearch.port'), - password: isSecurityEnabled - ? DEFAULT_SUPERUSER_PASS - : config.get('servers.elasticsearch.password'), + password: isSecurityEnabled ? 'changeme' : config.get('servers.elasticsearch.password'), license, log, basePath: resolve(KIBANA_ROOT, '.es'), @@ -49,22 +45,5 @@ export async function runElasticsearch({ await cluster.start(); - if (isSecurityEnabled) { - await setupUsers({ - log, - esPort: config.get('servers.elasticsearch.port'), - updates: [config.get('servers.elasticsearch'), config.get('servers.kibana')], - protocol: config.get('servers.elasticsearch').protocol, - caPath: getRelativeCertificateAuthorityPath(config.get('kbnTestServer.serverArgs')), - }); - } - return cluster; } - -function getRelativeCertificateAuthorityPath(esConfig: string[] = []) { - const caConfig = esConfig.find( - (config) => config.indexOf('--elasticsearch.ssl.certificateAuthorities') === 0 - ); - return caConfig ? caConfig.split('=')[1] : undefined; -} diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index dd5343b0118b3..af100a33ea3a7 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -29,8 +29,6 @@ export { esTestConfig, createTestEsCluster } from './es'; export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn'; -export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth'; - export { readConfigFile } from './functional_test_runner/lib/config/read_config_file'; export { runFtrCli } from './functional_test_runner/cli'; diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index ba22ecb3b6376..2995ffd08e5c0 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -7,15 +7,7 @@ */ import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; -import { - createTestEsCluster, - DEFAULT_SUPERUSER_PASS, - esTestConfig, - kbnTestConfig, - kibanaServerTestUser, - kibanaTestUser, - setupUsers, -} from '@kbn/test'; +import { createTestEsCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test'; import { defaultsDeep } from 'lodash'; import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; @@ -208,7 +200,6 @@ export function createTestServers({ defaultsDeep({}, settings.es ?? {}, { log, license, - password: license === 'trial' ? DEFAULT_SUPERUSER_PASS : undefined, }) ); @@ -224,19 +215,7 @@ export function createTestServers({ await es.start(); if (['gold', 'trial'].includes(license)) { - await setupUsers({ - log, - esPort: esTestConfig.getUrlParts().port, - updates: [ - ...usersToBeAdded, - // user elastic - esTestConfig.getUrlParts() as { username: string; password: string }, - // user kibana - kbnTestConfig.getUrlParts() as { username: string; password: string }, - ], - }); - - // Override provided configs, we know what the elastic user is now + // Override provided configs kbnSettings.elasticsearch = { hosts: [esTestConfig.getUrl()], username: kibanaServerTestUser.username, From 7f7ba0b560f3173a4a51abaf31a8125fff4bebb4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 20:49:52 -0400 Subject: [PATCH 031/118] [Fleet] Update final pipeline based on ECS event.agent_id_status (#102805) (#102832) This updates the Fleet final pipeline added in #100973 to match the specification of `event.agent_id_status` field as defined in ECS. The field was added to ECS in https://github.com/elastic/ecs/pull/1454. Basically the values of the field were simplified from what was originally proposed and implemented. Co-authored-by: Andrew Kroh --- .../ingest_pipeline/final_pipeline.ts | 25 ++++++++++--------- .../apis/epm/final_pipeline.ts | 8 +++--- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts index 4c0484c058abf..f929a4f139981 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts @@ -59,25 +59,26 @@ processors: } String verified(def ctx, def params) { - // Agents only use API keys. - if (ctx?._security?.authentication_type == null || ctx._security.authentication_type != 'API_KEY') { - return "no_api_key"; + // No agent.id field to validate. + if (ctx?.agent?.id == null) { + return "missing"; } - // Verify the API key owner before trusting any metadata it contains. - if (!is_user_trusted(ctx, params.trusted_users)) { - return "untrusted_user"; - } - - // API keys created by Fleet include metadata about the agent they were issued to. - if (ctx?._security?.api_key?.metadata?.agent_id == null || ctx?.agent?.id == null) { - return "missing_metadata"; + // Check auth metadata from API key. + if (ctx?._security?.authentication_type == null + // Agents only use API keys. + || ctx._security.authentication_type != 'API_KEY' + // Verify the API key owner before trusting any metadata it contains. + || !is_user_trusted(ctx, params.trusted_users) + // Verify the API key has metadata indicating the assigned agent ID. + || ctx?._security?.api_key?.metadata?.agent_id == null) { + return "auth_metadata_missing"; } // The API key can only be used represent the agent.id it was issued to. if (ctx._security.api_key.metadata.agent_id != ctx.agent.id) { // Potential masquerade attempt. - return "agent_id_mismatch"; + return "mismatch"; } return "verified"; diff --git a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts index a800546a27a3e..81f712e095c78 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts @@ -112,14 +112,14 @@ export default function (providerContext: FtrProviderContext) { // @ts-expect-error const event = doc._source.event; - expect(event.agent_id_status).to.be('no_api_key'); + expect(event.agent_id_status).to.be('auth_metadata_missing'); expect(event).to.have.property('ingested'); }); const scenarios = [ { name: 'API key without metadata', - expectedStatus: 'missing_metadata', + expectedStatus: 'auth_metadata_missing', event: { agent: { id: 'agent1' } }, }, { @@ -134,7 +134,7 @@ export default function (providerContext: FtrProviderContext) { }, { name: 'API key with agent id metadata and no agent id in event', - expectedStatus: 'missing_metadata', + expectedStatus: 'missing', apiKey: { metadata: { agent_id: 'agent1', @@ -143,7 +143,7 @@ export default function (providerContext: FtrProviderContext) { }, { name: 'API key with agent id metadata and tampered agent id in event', - expectedStatus: 'agent_id_mismatch', + expectedStatus: 'mismatch', apiKey: { metadata: { agent_id: 'agent2', From 86d5eeab4bf53e747b3404fec8ed2619b118c48c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 21 Jun 2021 23:23:24 -0400 Subject: [PATCH 032/118] [Fleet] Correctly check for degraded status in agent healthbar (#102821) (#102842) Co-authored-by: Nicolas Chaulet --- .../fleet/common/services/agent_status.ts | 2 +- .../apis/fleet_telemetry.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index df5de6ad98191..b8a59e6447723 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -54,7 +54,7 @@ export function buildKueryForOnlineAgents() { } export function buildKueryForErrorAgents() { - return 'last_checkin_status:error or .last_checkin_status:degraded'; + return 'last_checkin_status:error or last_checkin_status:degraded'; } export function buildKueryForOfflineAgents() { diff --git a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts index 5e4a580473dd1..36eef019f7bf7 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts @@ -21,9 +21,12 @@ export default function (providerContext: FtrProviderContext) { let data: any = {}; switch (status) { - case 'unhealthy': + case 'error': data = { last_checkin_status: 'error' }; break; + case 'degraded': + data = { last_checkin_status: 'degraded' }; + break; case 'offline': data = { last_checkin: '2017-06-07T18:59:04.498Z' }; break; @@ -85,12 +88,13 @@ export default function (providerContext: FtrProviderContext) { // Default Fleet Server await generateAgent('healthy', defaultFleetServerPolicy.id); await generateAgent('healthy', defaultFleetServerPolicy.id); - await generateAgent('unhealthy', defaultFleetServerPolicy.id); + await generateAgent('error', defaultFleetServerPolicy.id); // Default policy await generateAgent('healthy', defaultServerPolicy.id); await generateAgent('offline', defaultServerPolicy.id); - await generateAgent('unhealthy', defaultServerPolicy.id); + await generateAgent('error', defaultServerPolicy.id); + await generateAgent('degraded', defaultServerPolicy.id); }); it('should return the correct telemetry values for fleet', async () => { @@ -105,12 +109,12 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(apiResponse.stack_stats.kibana.plugins.fleet.agents).eql({ - total_enrolled: 6, + total_enrolled: 7, healthy: 3, - unhealthy: 2, + unhealthy: 3, offline: 1, updating: 0, - total_all_statuses: 6, + total_all_statuses: 7, }); expect(apiResponse.stack_stats.kibana.plugins.fleet.fleet_server).eql({ From e26582638988179d134e77e59b66ed8f982ab064 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 21 Jun 2021 20:26:56 -0700 Subject: [PATCH 033/118] [Workplace Search] Convert Sources pages to new page template (+ personal dashboard) (#102592) (#102843) * Refactor PersonalDashboardLayout to more closely match new page template - Remove references to enterpriseSearchLayout CSS (which will be removed in an upcoming PR) - Prefer to lean more heavily on default EuiPage props/CSS/etc. - Handle conditional sidebar logic in this layout rather than passing it in as a prop - Update props & DRY concerns to more closely match WorkplaceSearchPageTemplate - e.g. isLoading & pageChrome (mostly for document titles) - make FlashMessage and readOnlyMode work OOTB w/o props) * Convert Source subnav to EuiSideNav format + update PrivateSourcesSidebar to use EuiSIdeNav * Update routers - removing wrapping layouts, flash messages, chrome/telemetry * Refactor SourceRouter into shared layout component - Remove license callout, page header, and page chrome/telemetry - NOTE: The early page isLoading behavior (lines 51-) is required to prevent a flash of a completely empty page (instead of preserving the layout/side nav while loading). We cannot let the page fall through to the route because some routes are conditionally rendered based on isCustomSource. - FWIW: App Search has a similar isLoading early return with its Engine sub nav, and also a similar AnalyticsLayout for DRYing out repeated concerns/UI elements within Analytics subroutes. * Convert all single source views to new page template - Mostly removing isLoading tests - NOTE: Schema page could *possibly* use the new isEmptyState/emptyState page template props, but would need some layout reshuffling * Convert Add Source pages to conditional page templates - Opted to give these pages their own conditional layout logic - this could possibly be DRY'd out - There is possibly extra cleanup here on this file that could have been done (e.g. empty state, titles, etc.) in light of the new templates - but I didn't want to spend extra time here and went with creating as few diffs as possible * Convert separate Organization Sources & Private Sources views to new page templates + fix Link to EuiButtonTo on Organization Sources view * Update Account Settings with personal layout + write tests + add related KibanaLogic branch coverage * [UX feedback] Do not render page headers while loading on Overview & Sources pages * [PR feedback] Breadcrumb errors/fallbacks * [Proposal] Update schema errors routing to better work with nav/breadcrumbs - `exact` is required to make the parent schemas/ not gobble schema/{errorId} - added bonus breadcrumb for nicer schema navigation UX - No tests need to update AFAICT * Ignore Typescript error on soon-to-come EUI prop # Conflicts: # x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx --- .../shared/kibana/kibana_logic.test.ts | 6 + .../components/layout/nav.test.tsx | 3 + .../components/layout/nav.tsx | 3 +- .../personal_dashboard_layout.scss | 24 ++-- .../personal_dashboard_layout.test.tsx | 81 ++++++++++-- .../personal_dashboard_layout.tsx | 65 ++++++---- .../private_sources_sidebar.test.tsx | 53 +++++--- .../private_sources_sidebar.tsx | 14 ++- .../applications/workplace_search/index.tsx | 44 ++----- .../workplace_search/routes.test.tsx | 4 +- .../applications/workplace_search/routes.ts | 2 +- .../account_settings.test.tsx | 57 +++++++++ .../account_settings/account_settings.tsx | 6 +- .../components/add_source/add_source.test.tsx | 27 +++- .../components/add_source/add_source.tsx | 14 ++- .../add_source/add_source_list.test.tsx | 95 ++++++++------ .../components/add_source/add_source_list.tsx | 27 ++-- .../display_settings.test.tsx | 8 -- .../display_settings/display_settings.tsx | 14 ++- .../components/overview.test.tsx | 10 -- .../content_sources/components/overview.tsx | 12 +- .../components/schema/schema.test.tsx | 10 +- .../components/schema/schema.tsx | 13 +- .../schema/schema_change_errors.tsx | 9 +- .../components/source_content.test.tsx | 8 -- .../components/source_content.tsx | 12 +- .../components/source_layout.test.tsx | 84 +++++++++++++ .../components/source_layout.tsx | 84 +++++++++++++ .../components/source_settings.tsx | 7 +- .../components/source_sub_nav.test.tsx | 94 +++++++++++--- .../components/source_sub_nav.tsx | 74 ++++++----- .../organization_sources.test.tsx | 16 +-- .../content_sources/organization_sources.tsx | 71 ++++++----- .../content_sources/private_sources.test.tsx | 8 -- .../views/content_sources/private_sources.tsx | 17 +-- .../content_sources/source_router.test.tsx | 101 ++++++--------- .../views/content_sources/source_router.tsx | 118 +++++------------- .../views/content_sources/sources_router.tsx | 110 +++++++--------- .../views/overview/overview.test.tsx | 7 ++ .../views/overview/overview.tsx | 12 +- .../components/source_config.test.tsx | 7 ++ .../settings/components/source_config.tsx | 2 +- 42 files changed, 881 insertions(+), 552 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx 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 4cc907c3de9e4..39392d0c5c78e 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 @@ -33,6 +33,12 @@ describe('KibanaLogic', () => { expect(KibanaLogic.values.config).toEqual({}); }); + it('gracefully handles disabled security', () => { + mountKibanaLogic({ ...mockKibanaValues, security: undefined } as any); + + expect(KibanaLogic.values.security).toEqual({}); + }); + it('gracefully handles non-cloud installs', () => { mountKibanaLogic({ ...mockKibanaValues, cloud: undefined } as any); 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 3d5d0a8e6f2cf..04b0880a7351c 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 @@ -9,6 +9,9 @@ jest.mock('../../../shared/layout', () => ({ ...jest.requireActual('../../../shared/layout'), generateNavLink: jest.fn(({ to }) => ({ href: to })), })); +jest.mock('../../views/content_sources/components/source_sub_nav', () => ({ + useSourceSubNav: () => [], +})); jest.mock('../../views/groups/components/group_sub_nav', () => ({ useGroupSubNav: () => [], })); 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 f59679e0ee048..99225bc36e892 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 @@ -19,6 +19,7 @@ import { GROUPS_PATH, ORG_SETTINGS_PATH, } from '../../routes'; +import { useSourceSubNav } from '../../views/content_sources/components/source_sub_nav'; import { useGroupSubNav } from '../../views/groups/components/group_sub_nav'; import { useSettingsSubNav } from '../../views/settings/components/settings_sub_nav'; @@ -33,7 +34,7 @@ export const useWorkplaceSearchNav = () => { id: 'sources', name: NAV.SOURCES, ...generateNavLink({ to: SOURCES_PATH }), - items: [], // TODO: Source subnav + items: useSourceSubNav(), }, { id: 'groups', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss index 175f6b9ebca20..3287cb21783cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss @@ -6,18 +6,20 @@ */ .personalDashboardLayout { - $sideBarWidth: $euiSize * 30; - $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes - $pageHeight: calc(100vh - #{$consoleHeaderHeight}); + &__sideBar { + padding: $euiSizeXL $euiSizeXXL $euiSizeXXL; - left: $sideBarWidth; - width: calc(100% - #{$sideBarWidth}); - min-height: $pageHeight; + @include euiBreakpoint('m', 'l') { + min-width: $euiSize * 20; + } + @include euiBreakpoint('xl') { + min-width: $euiSize * 30; + } + } - &__sideBar { - padding: 32px 40px 40px; - width: $sideBarWidth; - margin-left: -$sideBarWidth; - height: $pageHeight; + &__body { + position: relative; + width: 100%; + height: 100%; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx index faeaa7323e93f..6847e91d46f6e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx @@ -5,37 +5,102 @@ * 2.0. */ +import { setMockValues } from '../../../../__mocks__/kea_logic'; +import { mockUseRouteMatch } from '../../../../__mocks__/react_router'; + import React from 'react'; import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; -import { AccountHeader } from '..'; +import { FlashMessages } from '../../../../shared/flash_messages'; +import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome'; +import { Loading } from '../../../../shared/loading'; + +import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index'; import { PersonalDashboardLayout } from './personal_dashboard_layout'; describe('PersonalDashboardLayout', () => { const children =

test

; - const sidebar =

test

; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({ readOnlyMode: false }); + }); it('renders', () => { - const wrapper = shallow( - {children} - ); + const wrapper = shallow({children}); expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="TestSidebar"]')).toHaveLength(1); + expect(wrapper.find('.personalDashboardLayout')).toHaveLength(1); expect(wrapper.find(AccountHeader)).toHaveLength(1); + expect(wrapper.find(FlashMessages)).toHaveLength(1); }); - it('renders callout when in read-only mode', () => { + describe('renders sidebar content based on the route', () => { + it('renders the private sources sidebar on the private sources path', () => { + (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/sources'); + const wrapper = shallow({children}); + + expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(1); + }); + + it('renders the account settings sidebar on the account settings path', () => { + (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/settings'); + const wrapper = shallow({children}); + + expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(1); + }); + + it('does not render a sidebar if not on a valid personal dashboard path', () => { + (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/test'); + const wrapper = shallow({children}); + + expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(0); + expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(0); + }); + }); + + describe('loading state', () => { + it('renders a loading icon in place of children', () => { + const wrapper = shallow( + {children} + ); + + expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(0); + }); + + it('renders children & does not render a loading icon when the page is done loading', () => { + const wrapper = shallow( + {children} + ); + + expect(wrapper.find(Loading)).toHaveLength(0); + expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + }); + }); + + it('sets WS page chrome (primarily document title)', () => { const wrapper = shallow( - + {children} ); + expect(wrapper.find(SetWorkplaceSearchChrome).prop('trail')).toEqual([ + 'Sources', + 'Add source', + 'Gmail', + ]); + }); + + it('renders callout when in read-only mode', () => { + setMockValues({ readOnlyMode: true }); + const wrapper = shallow({children}); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx index 1ab9e07dfa14d..5b68d661ac5df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx @@ -6,44 +6,67 @@ */ import React from 'react'; +import { useRouteMatch } from 'react-router-dom'; -import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; +import { useValues } from 'kea'; -import { AccountHeader } from '..'; +import { + EuiPage, + EuiPageSideBar, + EuiPageBody, + EuiPageContentBody, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { FlashMessages } from '../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../shared/http'; +import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome'; +import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; +import { Loading } from '../../../../shared/loading'; + +import { PERSONAL_SOURCES_PATH, PERSONAL_SETTINGS_PATH } from '../../../routes'; import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING } from '../../../views/content_sources/constants'; +import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index'; import './personal_dashboard_layout.scss'; interface LayoutProps { - restrictWidth?: boolean; - readOnlyMode?: boolean; - sidebar: React.ReactNode; + isLoading?: boolean; + pageChrome?: BreadcrumbTrail; } export const PersonalDashboardLayout: React.FC = ({ children, - restrictWidth, - readOnlyMode, - sidebar, + isLoading, + pageChrome, }) => { + const { readOnlyMode } = useValues(HttpLogic); + return ( <> + {pageChrome && } - - - {sidebar} + + + {useRouteMatch(PERSONAL_SOURCES_PATH) && } + {useRouteMatch(PERSONAL_SETTINGS_PATH) && } - - {readOnlyMode && ( - - )} - {children} + + + {readOnlyMode && ( + <> + + + + )} + + {isLoading ? : children} + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx index 387724af970f8..9fa4d4dd1b237 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx @@ -7,17 +7,22 @@ import { setMockValues } from '../../../../__mocks__/kea_logic'; +jest.mock('../../../views/content_sources/components/source_sub_nav', () => ({ + useSourceSubNav: () => [], +})); + import React from 'react'; import { shallow } from 'enzyme'; +import { EuiSideNav } from '@elastic/eui'; + import { PRIVATE_CAN_CREATE_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION, PRIVATE_CAN_CREATE_PAGE_DESCRIPTION, } from '../../../constants'; -import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav'; import { ViewContentHeader } from '../../shared/view_content_header'; @@ -26,6 +31,7 @@ import { PrivateSourcesSidebar } from './private_sources_sidebar'; describe('PrivateSourcesSidebar', () => { const mockValues = { account: { canCreatePersonalSources: true }, + contentSource: {}, }; beforeEach(() => { @@ -36,25 +42,42 @@ describe('PrivateSourcesSidebar', () => { const wrapper = shallow(); expect(wrapper.find(ViewContentHeader)).toHaveLength(1); - expect(wrapper.find(SourceSubNav)).toHaveLength(1); }); - it('uses correct title and description when private sources are enabled', () => { - const wrapper = shallow(); + describe('header text', () => { + it('uses correct title and description when private sources are enabled', () => { + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE); + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + PRIVATE_CAN_CREATE_PAGE_DESCRIPTION + ); + }); - expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - PRIVATE_CAN_CREATE_PAGE_DESCRIPTION - ); + it('uses correct title and description when private sources are disabled', () => { + setMockValues({ ...mockValues, account: { canCreatePersonalSources: false } }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE); + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION + ); + }); }); - it('uses correct title and description when private sources are disabled', () => { - setMockValues({ account: { canCreatePersonalSources: false } }); - const wrapper = shallow(); + describe('sub nav', () => { + it('renders a side nav when viewing a single source', () => { + setMockValues({ ...mockValues, contentSource: { id: '1', name: 'test source' } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSideNav)).toHaveLength(1); + }); + + it('does not render a side nav if not on a source page', () => { + setMockValues({ ...mockValues, contentSource: {} }); + const wrapper = shallow(); - expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION - ); + expect(wrapper.find(EuiSideNav)).toHaveLength(0); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx index 5505ae57b2ad5..36496b83b3123 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { useValues } from 'kea'; +import { EuiSideNav } from '@elastic/eui'; + import { AppLogic } from '../../../app_logic'; import { PRIVATE_CAN_CREATE_PAGE_TITLE, @@ -16,7 +18,8 @@ import { PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION, PRIVATE_CAN_CREATE_PAGE_DESCRIPTION, } from '../../../constants'; -import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav'; +import { useSourceSubNav } from '../../../views/content_sources/components/source_sub_nav'; +import { SourceLogic } from '../../../views/content_sources/source_logic'; import { ViewContentHeader } from '../../shared/view_content_header'; export const PrivateSourcesSidebar = () => { @@ -31,10 +34,17 @@ export const PrivateSourcesSidebar = () => { ? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION : PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION; + const { + contentSource: { id = '', name = '' }, + } = useValues(SourceLogic); + + const navItems = [{ id, name, items: useSourceSubNav() }]; + return ( <> - + {/* @ts-expect-error: TODO, uncomment this once EUI 34.x lands in Kibana & `mobileBreakpoints` is a valid prop */} + {id && } ); }; 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 f4278d5083143..8a1e9c0275322 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 @@ -19,11 +19,6 @@ import { NotFound } from '../shared/not_found'; import { AppLogic } from './app_logic'; import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; -import { - PersonalDashboardLayout, - PrivateSourcesSidebar, - AccountSettingsSidebar, -} from './components/layout'; import { GROUPS_PATH, SETUP_GUIDE_PATH, @@ -34,11 +29,11 @@ import { ROLE_MAPPINGS_PATH, SECURITY_PATH, PERSONAL_SETTINGS_PATH, + PERSONAL_PATH, } from './routes'; import { AccountSettings } from './views/account_settings'; import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; -import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; import { ErrorState } from './views/error_state'; import { GroupsRouter } from './views/groups'; import { Overview } from './views/overview'; @@ -60,9 +55,6 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { const { pathname } = useLocation(); - // We don't want so show the subnavs on the container root pages. - const showSourcesSubnav = pathname !== SOURCES_PATH && pathname !== PERSONAL_SOURCES_PATH; - /** * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources @@ -95,32 +87,18 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { - - } - > - - - - - } - > - - + + + + + + + + + - } />} - restrictWidth - readOnlyMode={readOnlyMode} - > - - + 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 a5a3d6b491bb9..b89a1451f7e57 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 @@ -76,13 +76,13 @@ describe('getReindexJobRoute', () => { it('should format org path', () => { expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, true)).toEqual( - `/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}` + `/sources/${SOURCE_ID}/schemas/${REINDEX_ID}` ); }); it('should format user path', () => { expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, false)).toEqual( - `/p/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}` + `/p/sources/${SOURCE_ID}/schemas/${REINDEX_ID}` ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 1fe8019c4b364..3c564c1f912ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -88,7 +88,7 @@ export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`; export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`; export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display_settings`; export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`; -export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema_errors/:activeReindexJobId`; +export const REINDEX_JOB_PATH = `${SOURCE_SCHEMAS_PATH}/:activeReindexJobId`; export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`; export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result_detail`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx new file mode 100644 index 0000000000000..5ff80a7683db6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx @@ -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 '../../../__mocks__/shallow_useeffect.mock'; +import { mockKibanaValues } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { AccountSettings } from './'; + +describe('AccountSettings', () => { + const { + security: { + authc: { getCurrentUser }, + uiApi: { + components: { getPersonalInfo, getChangePassword }, + }, + }, + } = mockKibanaValues; + + const mockCurrentUser = (user?: unknown) => + (getCurrentUser as jest.Mock).mockReturnValue(Promise.resolve(user)); + + beforeAll(() => { + mockCurrentUser(); + }); + + it('gets the current user on mount', () => { + shallow(); + + expect(getCurrentUser).toHaveBeenCalled(); + }); + + it('does not render if the current user does not exist', async () => { + mockCurrentUser(null); + const wrapper = await shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders the security UI components when the user exists', async () => { + mockCurrentUser({ username: 'mock user' }); + (getPersonalInfo as jest.Mock).mockReturnValue(
); + (getChangePassword as jest.Mock).mockReturnValue(
); + + const wrapper = await shallow(); + + expect(wrapper.childAt(0).dive().find('[data-test-subj="PersonalInfo"]')).toHaveLength(1); + expect(wrapper.childAt(1).dive().find('[data-test-subj="ChangePassword"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx index e28faaeec8993..313d3ffa59d48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx @@ -11,6 +11,8 @@ import { useValues } from 'kea'; import type { AuthenticatedUser } from '../../../../../../security/public'; import { KibanaLogic } from '../../../shared/kibana/kibana_logic'; +import { PersonalDashboardLayout } from '../../components/layout'; +import { ACCOUNT_SETTINGS_TITLE } from '../../constants'; export const AccountSettings: React.FC = () => { const { security } = useValues(KibanaLogic); @@ -31,9 +33,9 @@ export const AccountSettings: React.FC = () => { } return ( - <> + - + ); }; 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 92cbfcf6eeafe..0501509b3a8ef 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 @@ -17,7 +17,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Loading } from '../../../../../shared/loading'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; import { AddSource } from './add_source'; import { AddSourceSteps } from './add_source_logic'; @@ -68,11 +71,27 @@ describe('AddSourceList', () => { expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep); }); - it('handles loading state', () => { - setMockValues({ ...mockValues, dataLoading: true }); + describe('layout', () => { + it('renders the default workplace search layout when on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + }); + + it('renders the personal dashboard layout when not in an organization', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + }); + }); + + it('renders a breadcrumb fallback while data is loading', () => { + setMockValues({ ...mockValues, dataLoading: true, sourceConfigData: {} }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('pageChrome')).toEqual(['Sources', 'Add Source', '...']); }); it('renders Config Completed step', () => { 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 ee4bcfb9afd34..b0c3ebe64830c 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 @@ -13,9 +13,12 @@ import { i18n } from '@kbn/i18n'; import { setSuccessMessage } from '../../../../../shared/flash_messages'; import { KibanaLogic } from '../../../../../shared/kibana'; -import { Loading } from '../../../../../shared/loading'; import { AppLogic } from '../../../../app_logic'; -import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants'; import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; import { SourceDataItem } from '../../../../types'; import { staticSourceData } from '../../source_data'; @@ -71,8 +74,6 @@ export const AddSource: React.FC = (props) => { return resetSourceState; }, []); - if (dataLoading) return ; - const goToConfigurationIntro = () => setAddSourceStep(AddSourceSteps.ConfigIntroStep); const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep); const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep); @@ -99,9 +100,10 @@ export const AddSource: React.FC = (props) => { }; const header = ; + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; return ( - <> + {addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && ( )} @@ -158,6 +160,6 @@ export const AddSource: React.FC = (props) => { {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( )} - + ); }; 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 6bf71cd73ec35..b30511f0a6d80 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 @@ -19,7 +19,11 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; +import { getPageDescription } from '../../../../../test_helpers'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { AddSourceList } from './add_source_list'; @@ -54,14 +58,21 @@ describe('AddSourceList', () => { expect(wrapper.find(AvailableSourcesList)).toHaveLength(1); }); - it('returns loading when loading', () => { - setMockValues({ - ...mockValues, - dataLoading: true, + describe('layout', () => { + it('renders the default workplace search layout when on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); }); - const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + it('renders the personal dashboard layout and a header when not in an organization', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + }); }); describe('filters sources', () => { @@ -97,49 +108,51 @@ describe('AddSourceList', () => { }); describe('content headings', () => { - it('should render correct organization heading with sources', () => { - const wrapper = shallow(); - - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_ORG_SOURCE_DESCRIPTION - ); - }); + describe('organization view', () => { + it('should render the correct organization heading with sources', () => { + const wrapper = shallow(); - it('should render correct organization heading without sources', () => { - setMockValues({ - ...mockValues, - contentSources: [], + expect(getPageDescription(wrapper)).toEqual(ADD_SOURCE_ORG_SOURCE_DESCRIPTION); }); - const wrapper = shallow(); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION - ); - }); + it('should render the correct organization heading without sources', () => { + setMockValues({ + ...mockValues, + contentSources: [], + }); + const wrapper = shallow(); - it('should render correct account heading with sources', () => { - const wrapper = shallow(); - setMockValues({ - ...mockValues, - isOrganization: false, + expect(getPageDescription(wrapper)).toEqual( + ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION + ); }); - - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_ORG_SOURCE_DESCRIPTION - ); }); - it('should render correct account heading without sources', () => { - setMockValues({ - ...mockValues, - isOrganization: false, - contentSources: [], + describe('personal dashboard view', () => { + it('should render the correct personal heading with sources', () => { + setMockValues({ + ...mockValues, + isOrganization: false, + }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION + ); }); - const wrapper = shallow(); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( - ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION - ); + it('should render the correct personal heading without sources', () => { + setMockValues({ + ...mockValues, + isOrganization: false, + contentSources: [], + }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION + ); + }); }); }); 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 80d35553bb8bb..a7a64194cb42f 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 @@ -19,12 +19,15 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { AppLogic } from '../../../../app_logic'; import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; import { ContentSection } from '../../../../components/shared/content_section'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants'; import { SourceDataItem } from '../../../../types'; import { SourcesLogic } from '../../sources_logic'; @@ -58,8 +61,6 @@ export const AddSourceList: React.FC = () => { return resetSourcesState; }, []); - if (dataLoading) return ; - const hasSources = contentSources.length > 0; const showConfiguredSourcesList = configuredSources.find( ({ serviceType }) => serviceType !== CUSTOM_SERVICE_TYPE @@ -97,12 +98,22 @@ export const AddSourceList: React.FC = () => { filterConfiguredSources ) as SourceDataItem[]; + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + return ( - <> - + + {!isOrganization && ( +
+ +
+ )} {showConfiguredSourcesList || isOrganization ? ( - { )} - +
); }; 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 aa5cec385738d..e5714bf4bdfbf 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 @@ -16,7 +16,6 @@ import { shallow } from 'enzyme'; import { EuiButton, EuiTabbedContent } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; @@ -57,13 +56,6 @@ describe('DisplaySettings', () => { expect(wrapper.find('form')).toHaveLength(1); }); - it('returns loading when loading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - describe('tabbed content', () => { const tabs = [ { 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 d923fbe7a1a8e..ae47e20026b68 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 @@ -20,10 +20,10 @@ import { } from '@elastic/eui'; import { clearFlashMessages } from '../../../../../shared/flash_messages'; -import { Loading } from '../../../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { SAVE_BUTTON } from '../../../../constants'; +import { NAV, SAVE_BUTTON } from '../../../../constants'; +import { SourceLayout } from '../source_layout'; import { UNSAVED_MESSAGE, @@ -64,8 +64,6 @@ export const DisplaySettings: React.FC = ({ tabId }) => { return clearFlashMessages; }, []); - if (dataLoading) return ; - const tabs = [ { id: 'search_results', @@ -89,7 +87,11 @@ export const DisplaySettings: React.FC = ({ tabId }) => { }; return ( - <> + = ({ tabId }) => { )} {addFieldModalVisible && } - + ); }; 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 f2cf5f50b813b..d99eac5de74e5 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 @@ -5,8 +5,6 @@ * 2.0. */ -import '../../../../__mocks__/shallow_useeffect.mock'; - import { setMockValues } from '../../../../__mocks__/kea_logic'; import { fullContentSources } from '../../../__mocks__/content_sources.mock'; @@ -16,7 +14,6 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui'; -import { Loading } from '../../../../shared/loading'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { Overview } from './overview'; @@ -44,13 +41,6 @@ describe('Overview', () => { expect(documentSummary.find('[data-test-subj="DocumentSummaryRow"]')).toHaveLength(1); }); - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders ComponentLoader when loading', () => { setMockValues({ ...mockValues, 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 153df1bc00496..cc890e0f104ac 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 @@ -29,7 +29,6 @@ import { } 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'; @@ -78,8 +77,10 @@ import { } from '../constants'; import { SourceLogic } from '../source_logic'; +import { SourceLayout } from './source_layout'; + export const Overview: React.FC = () => { - const { contentSource, dataLoading } = useValues(SourceLogic); + const { contentSource } = useValues(SourceLogic); const { isOrganization } = useValues(AppLogic); const { @@ -97,8 +98,6 @@ export const Overview: React.FC = () => { isFederatedSource, } = contentSource; - if (dataLoading) return ; - const DocumentSummary = () => { let totalDocuments = 0; const tableContent = summary?.map((item, index) => { @@ -450,8 +449,9 @@ export const Overview: React.FC = () => { ); return ( - <> + + @@ -513,6 +513,6 @@ 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 178c9125ee437..47859e4e67b17 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 @@ -16,7 +16,6 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema'; import { Schema } from './schema'; @@ -71,13 +70,6 @@ describe('Schema', () => { expect(wrapper.find(SchemaFieldsTable)).toHaveLength(1); }); - it('returns loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('handles empty state', () => { setMockValues({ ...mockValues, activeSchema: {} }); const wrapper = shallow(); @@ -106,7 +98,7 @@ describe('Schema', () => { expect(wrapper.find(SchemaErrorsCallout)).toHaveLength(1); expect(wrapper.find(SchemaErrorsCallout).prop('viewErrorsPath')).toEqual( - '/sources/123/schema_errors/123' + '/sources/123/schemas/123' ); }); }); 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 65ed988f45ff0..a0efebdcb5a48 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,11 +20,12 @@ import { EuiPanel, } from '@elastic/eui'; -import { Loading } from '../../../../../shared/loading'; import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema'; import { AppLogic } from '../../../../app_logic'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { NAV } from '../../../../constants'; import { getReindexJobRoute } from '../../../../routes'; +import { SourceLayout } from '../source_layout'; import { SCHEMA_ADD_FIELD_BUTTON, @@ -65,8 +66,6 @@ export const Schema: React.FC = () => { initializeSchema(); }, []); - if (dataLoading) return ; - const hasSchemaFields = Object.keys(activeSchema).length > 0; const { hasErrors, activeReindexJobId } = mostRecentIndexJob; @@ -77,7 +76,11 @@ export const Schema: React.FC = () => { ); return ( - <> + { closeAddFieldModal={closeAddFieldModal} /> )} - + ); }; 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 e300823aa3ed3..eb07beda73327 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 @@ -12,6 +12,8 @@ import { useActions, useValues } from 'kea'; import { SchemaErrorsAccordion } from '../../../../../shared/schema'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { NAV } from '../../../../constants'; +import { SourceLayout } from '../source_layout'; import { SCHEMA_ERRORS_HEADING } from './constants'; import { SchemaLogic } from './schema_logic'; @@ -30,9 +32,12 @@ export const SchemaChangeErrors: React.FC = () => { }, []); return ( - <> + - + ); }; 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 4bcc4b16166d1..9304f0f344a1b 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 @@ -25,7 +25,6 @@ import { } from '@elastic/eui'; import { DEFAULT_META } from '../../../../shared/constants'; -import { Loading } from '../../../../shared/loading'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; @@ -61,13 +60,6 @@ describe('SourceContent', () => { expect(wrapper.find(EuiTable)).toHaveLength(1); }); - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('returns ComponentLoader when section loading', () => { setMockValues({ ...mockValues, sectionLoading: true }); const wrapper = shallow(); 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 fbafe54df7493..a0e3c28f20eb0 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 @@ -31,12 +31,11 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Loading } from '../../../../shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { CUSTOM_SERVICE_TYPE } from '../../../constants'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; import { SourceContentItem } from '../../../types'; import { @@ -51,6 +50,8 @@ import { } from '../constants'; import { SourceLogic } from '../source_logic'; +import { SourceLayout } from './source_layout'; + const MAX_LENGTH = 28; export const SourceContent: React.FC = () => { @@ -67,7 +68,6 @@ export const SourceContent: React.FC = () => { }, contentItems, contentFilterValue, - dataLoading, sectionLoading, } = useValues(SourceLogic); @@ -75,8 +75,6 @@ export const SourceContent: React.FC = () => { searchContentSourceDocuments(id); }, [contentFilterValue, activePage]); - if (dataLoading) return ; - const showPagination = totalPages > 1; const hasItems = totalItems > 0; const emptyMessage = contentFilterValue @@ -193,7 +191,7 @@ export const SourceContent: React.FC = () => { ); return ( - <> + @@ -219,6 +217,6 @@ export const SourceContent: React.FC = () => { {sectionLoading && } {!sectionLoading && (hasItems ? contentTable : emptyState)} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx new file mode 100644 index 0000000000000..7c7d77ec418e7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../../__mocks__/kea_logic'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCallOut } from '@elastic/eui'; + +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout'; + +import { SourceInfoCard } from './source_info_card'; +import { SourceLayout } from './source_layout'; + +describe('SourceLayout', () => { + const contentSource = contentSources[1]; + const mockValues = { + contentSource, + dataLoading: false, + isOrganization: true, + }; + + beforeEach(() => { + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow( + +
+ + ); + + expect(wrapper.find(SourceInfoCard)).toHaveLength(1); + expect(wrapper.find('.testChild')).toHaveLength(1); + }); + + it('renders the default Workplace Search layout when on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + }); + + it('renders a personal dashboard layout when not on an organization view', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + }); + + it('passes any page template props to the underlying page template', () => { + const wrapper = shallow(); + + expect(wrapper.find(WorkplaceSearchPageTemplate).prop('pageViewTelemetry')).toEqual('test'); + }); + + it('handles breadcrumbs while loading', () => { + setMockValues({ + ...mockValues, + contentSource: {}, + dataLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.prop('pageChrome')).toEqual(['Sources', '...']); + }); + + it('renders a callout when the source is not supported by the current license', () => { + setMockValues({ ...mockValues, contentSource: { supportedByLicense: false } }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx new file mode 100644 index 0000000000000..446e93e0c61f3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; +import moment from 'moment'; + +import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; + +import { PageTemplateProps } from '../../../../shared/layout'; +import { AppLogic } from '../../../app_logic'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout'; +import { NAV } from '../../../constants'; +import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; + +import { + SOURCE_DISABLED_CALLOUT_TITLE, + SOURCE_DISABLED_CALLOUT_DESCRIPTION, + SOURCE_DISABLED_CALLOUT_BUTTON, +} from '../constants'; +import { SourceLogic } from '../source_logic'; + +import { SourceInfoCard } from './source_info_card'; + +export const SourceLayout: React.FC = ({ + children, + pageChrome = [], + ...props +}) => { + const { contentSource, dataLoading } = useValues(SourceLogic); + const { isOrganization } = useValues(AppLogic); + + const { + name, + createdAt, + serviceType, + serviceName, + isFederatedSource, + supportedByLicense, + } = contentSource; + + const pageHeader = ( + <> + + + + ); + + const callout = ( + <> + +

{SOURCE_DISABLED_CALLOUT_DESCRIPTION}

+ + {SOURCE_DISABLED_CALLOUT_BUTTON} + +
+ + + ); + + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + return ( + + {!supportedByLicense && callout} + {pageHeader} + {children} + + ); +}; 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 26c37c25dcfee..dab82a31d38f7 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 @@ -26,6 +26,8 @@ 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 { NAV } from '../../../constants'; + import { CANCEL_BUTTON, OK_BUTTON, @@ -52,6 +54,7 @@ import { staticSourceData } from '../source_data'; import { SourceLogic } from '../source_logic'; import { AddSourceLogic } from './add_source/add_source_logic'; +import { SourceLayout } from './source_layout'; export const SourceSettings: React.FC = () => { const { updateContentSource, removeContentSource } = useActions(SourceLogic); @@ -129,7 +132,7 @@ export const SourceSettings: React.FC = () => { ); return ( - <> +
@@ -198,6 +201,6 @@ export const SourceSettings: React.FC = () => { {confirmModalVisible && confirmModal} - + ); }; 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 25c389419d731..7f07c59587f96 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 @@ -7,34 +7,92 @@ import { setMockValues } from '../../../../__mocks__/kea_logic'; -import React from 'react'; +jest.mock('../../../../shared/layout', () => ({ + generateNavLink: jest.fn(({ to }) => ({ href: to })), +})); -import { shallow } from 'enzyme'; +import { useSourceSubNav } from './source_sub_nav'; -import { SideNavLink } from '../../../../shared/layout'; -import { CUSTOM_SERVICE_TYPE } from '../../../constants'; +describe('useSourceSubNav', () => { + it('returns undefined when no content source id present', () => { + setMockValues({ contentSource: {} }); -import { SourceSubNav } from './source_sub_nav'; + expect(useSourceSubNav()).toEqual(undefined); + }); -describe('SourceSubNav', () => { - it('renders empty when no group id present', () => { - setMockValues({ contentSource: {} }); - const wrapper = shallow(); + it('returns EUI nav items', () => { + setMockValues({ isOrganization: true, contentSource: { id: '1' } }); - expect(wrapper.find(SideNavLink)).toHaveLength(0); + expect(useSourceSubNav()).toEqual([ + { + id: 'sourceOverview', + name: 'Overview', + href: '/sources/1', + }, + { + id: 'sourceContent', + name: 'Content', + href: '/sources/1/content', + }, + { + id: 'sourceSettings', + name: 'Settings', + href: '/sources/1/settings', + }, + ]); }); - it('renders nav items', () => { - setMockValues({ contentSource: { id: '1' } }); - const wrapper = shallow(); + it('returns extra nav items for custom sources', () => { + setMockValues({ isOrganization: true, contentSource: { id: '2', serviceType: 'custom' } }); - expect(wrapper.find(SideNavLink)).toHaveLength(3); + expect(useSourceSubNav()).toEqual([ + { + id: 'sourceOverview', + name: 'Overview', + href: '/sources/2', + }, + { + id: 'sourceContent', + name: 'Content', + href: '/sources/2/content', + }, + { + id: 'sourceSchema', + name: 'Schema', + href: '/sources/2/schemas', + }, + { + id: 'sourceDisplaySettings', + name: 'Display Settings', + href: '/sources/2/display_settings', + }, + { + id: 'sourceSettings', + name: 'Settings', + href: '/sources/2/settings', + }, + ]); }); - it('renders custom source nav items', () => { - setMockValues({ contentSource: { id: '1', serviceType: CUSTOM_SERVICE_TYPE } }); - const wrapper = shallow(); + it('returns nav links to personal dashboard when not on an organization page', () => { + setMockValues({ isOrganization: false, contentSource: { id: '3' } }); - expect(wrapper.find(SideNavLink)).toHaveLength(5); + expect(useSourceSubNav()).toEqual([ + { + id: 'sourceOverview', + name: 'Overview', + href: '/p/sources/3', + }, + { + id: 'sourceContent', + name: 'Content', + href: '/p/sources/3/content', + }, + { + id: 'sourceSettings', + name: 'Settings', + href: '/p/sources/3/settings', + }, + ]); }); }); 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 12e1506ec6efd..6b595a06f0404 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 @@ -5,11 +5,11 @@ * 2.0. */ -import React from 'react'; - import { useValues } from 'kea'; -import { SideNavLink } from '../../../../shared/layout'; +import { EuiSideNavItemType } from '@elastic/eui'; + +import { generateNavLink } from '../../../../shared/layout'; import { AppLogic } from '../../../app_logic'; import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; import { @@ -22,40 +22,52 @@ import { } from '../../../routes'; import { SourceLogic } from '../source_logic'; -export const SourceSubNav: React.FC = () => { +export const useSourceSubNav = () => { const { isOrganization } = useValues(AppLogic); const { contentSource: { id, serviceType }, } = useValues(SourceLogic); - if (!id) return null; + if (!id) return undefined; + + const navItems: Array> = [ + { + id: 'sourceOverview', + name: NAV.OVERVIEW, + ...generateNavLink({ to: getContentSourcePath(SOURCE_DETAILS_PATH, id, isOrganization) }), + }, + { + id: 'sourceContent', + name: NAV.CONTENT, + ...generateNavLink({ to: getContentSourcePath(SOURCE_CONTENT_PATH, id, isOrganization) }), + }, + ]; const isCustom = serviceType === CUSTOM_SERVICE_TYPE; + if (isCustom) { + navItems.push({ + id: 'sourceSchema', + name: NAV.SCHEMA, + ...generateNavLink({ + to: getContentSourcePath(SOURCE_SCHEMAS_PATH, id, isOrganization), + shouldShowActiveForSubroutes: true, + }), + }); + navItems.push({ + id: 'sourceDisplaySettings', + name: NAV.DISPLAY_SETTINGS, + ...generateNavLink({ + to: getContentSourcePath(SOURCE_DISPLAY_SETTINGS_PATH, id, isOrganization), + shouldShowActiveForSubroutes: true, + }), + }); + } + + navItems.push({ + id: 'sourceSettings', + name: NAV.SETTINGS, + ...generateNavLink({ to: getContentSourcePath(SOURCE_SETTINGS_PATH, id, isOrganization) }), + }); - return ( -
- - {NAV.OVERVIEW} - - - {NAV.CONTENT} - - {isCustom && ( - <> - - {NAV.SCHEMA} - - - {NAV.DISPLAY_SETTINGS} - - - )} - - {NAV.SETTINGS} - -
- ); + return navItems; }; 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 9df91406c4b7b..2317c84af2432 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 @@ -10,14 +10,10 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; -import { Redirect } from 'react-router-dom'; 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'; @@ -42,20 +38,12 @@ describe('OrganizationSources', () => { const wrapper = shallow(); expect(wrapper.find(SourcesTable)).toHaveLength(1); - expect(wrapper.find(ViewContentHeader)).toHaveLength(1); }); - it('returns loading when loading', () => { + it('does not render a page header when data is loading (to prevent a jump after redirect)', () => { setMockValues({ ...mockValues, dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); - }); - - it('returns redirect when no sources', () => { - setMockValues({ ...mockValues, contentSources: [] }); - const wrapper = shallow(); - - expect(wrapper.find(Redirect).prop('to')).toEqual(getSourcesPath(ADD_SOURCE_PATH, true)); + expect(wrapper.prop('pageHeader')).toBeUndefined(); }); }); 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 4559003b4597f..a4273ae2ae6a2 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,16 +6,15 @@ */ import React, { useEffect } from 'react'; -import { Link, Redirect } from 'react-router-dom'; +import { Redirect } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiButton } from '@elastic/eui'; - -import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { WorkplaceSearchPageTemplate } from '../../components/layout'; import { ContentSection } from '../../components/shared/content_section'; import { SourcesTable } from '../../components/shared/sources_table'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { @@ -36,33 +35,41 @@ export const OrganizationSources: React.FC = () => { const { dataLoading, contentSources } = useValues(SourcesLogic); - if (dataLoading) return ; - - if (contentSources.length === 0) return ; - return ( - - - - {ORG_SOURCES_LINK} - - - } - description={ORG_SOURCES_HEADER_DESCRIPTION} - alignItems="flexStart" - /> - - - - - + + {ORG_SOURCES_LINK} + , + ], + } + } + isLoading={dataLoading} + isEmptyState={!contentSources.length} + emptyState={} + > + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx index 08f560c984344..e2b0dfba1fa97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx @@ -15,7 +15,6 @@ import { shallow } from 'enzyme'; import { EuiCallOut, EuiEmptyPrompt } from '@elastic/eui'; -import { Loading } from '../../../shared/loading'; import { ContentSection } from '../../components/shared/content_section'; import { SourcesTable } from '../../components/shared/sources_table'; @@ -43,13 +42,6 @@ describe('PrivateSources', () => { expect(wrapper.find(SourcesView)).toHaveLength(1); }); - it('renders Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders only shared sources section when canCreatePersonalSources is false', () => { setMockValues({ ...mockValues }); const wrapper = shallow(); 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 128c65eeb95da..693c1e8bd5e40 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,12 +13,13 @@ import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../shared/licensing'; -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 { PersonalDashboardLayout } from '../../components/layout'; import { ContentSection } from '../../components/shared/content_section'; import { SourcesTable } from '../../components/shared/sources_table'; +import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { toSentenceSerial } from '../../utils'; @@ -53,8 +54,6 @@ export const PrivateSources: React.FC = () => { account: { canCreatePersonalSources, groups }, } = useValues(AppLogic); - if (dataLoading) return ; - const hasConfiguredConnectors = serviceTypes.some(({ configured }) => configured); const canAddSources = canCreatePersonalSources && hasConfiguredConnectors; const hasPrivateSources = privateContentSources?.length > 0; @@ -144,10 +143,12 @@ export const PrivateSources: React.FC = () => { ); return ( - - {hasPrivateSources && !hasPlatinumLicense && licenseCallout} - {canCreatePersonalSources && privateSourcesSection} - {sharedSourcesSection} - + + + {hasPrivateSources && !hasPlatinumLicense && licenseCallout} + {canCreatePersonalSources && privateSourcesSection} + {sharedSourcesSection} + + ); }; 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 783fc434fe8e5..afe0d1f89faea 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 @@ -5,21 +5,17 @@ * 2.0. */ -import '../../../__mocks__/shallow_useeffect.mock'; - import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; -import { mockLocation, mockUseParams } from '../../../__mocks__/react_router'; +import { mockUseParams } from '../../../__mocks__/react_router'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Route } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; -import { NAV } from '../../constants'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; @@ -37,6 +33,7 @@ describe('SourceRouter', () => { const mockValues = { contentSource, dataLoading: false, + isOrganization: true, }; beforeEach(() => { @@ -50,11 +47,41 @@ describe('SourceRouter', () => { })); }); - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); - const wrapper = shallow(); + describe('mount/unmount events', () => { + it('fetches & initializes source data on mount', () => { + shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(initializeSource).toHaveBeenCalledWith(contentSource.id); + }); + + it('resets state on unmount', () => { + shallow(); + unmountHandler(); + + expect(resetSourceState).toHaveBeenCalled(); + }); + }); + + describe('loading state when fetching source data', () => { + // NOTE: The early page isLoading returns are required to prevent a flash of a completely empty + // page (instead of preserving the layout/side nav while loading). We also cannot let the code + // fall through to the router because some routes are conditionally rendered based on isCustomSource. + + it('returns an empty loading Workplace Search page on organization views', () => { + setMockValues({ ...mockValues, dataLoading: true, isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + expect(wrapper.prop('isLoading')).toEqual(true); + }); + + it('returns an empty loading personal dashboard page when not on an organization view', () => { + setMockValues({ ...mockValues, dataLoading: true, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + expect(wrapper.prop('isLoading')).toEqual(true); + }); }); it('renders source routes (standard)', () => { @@ -63,7 +90,6 @@ describe('SourceRouter', () => { expect(wrapper.find(Overview)).toHaveLength(1); expect(wrapper.find(SourceSettings)).toHaveLength(1); expect(wrapper.find(SourceContent)).toHaveLength(1); - expect(wrapper.find(Switch)).toHaveLength(1); expect(wrapper.find(Route)).toHaveLength(3); }); @@ -76,55 +102,4 @@ describe('SourceRouter', () => { expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1); expect(wrapper.find(Route)).toHaveLength(6); }); - - it('handles breadcrumbs while loading (standard)', () => { - setMockValues({ - ...mockValues, - contentSource: {}, - }); - - const loadingBreadcrumbs = ['Sources', '...']; - - const wrapper = shallow(); - - const overviewBreadCrumb = wrapper.find(SetPageChrome).at(0); - const contentBreadCrumb = wrapper.find(SetPageChrome).at(1); - const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2); - - expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs]); - expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]); - expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]); - }); - - it('handles breadcrumbs while loading (custom)', () => { - setMockValues({ - ...mockValues, - contentSource: { serviceType: 'custom' }, - }); - - const loadingBreadcrumbs = ['Sources', '...']; - - const wrapper = shallow(); - - const schemaBreadCrumb = wrapper.find(SetPageChrome).at(2); - const schemaErrorsBreadCrumb = wrapper.find(SetPageChrome).at(3); - const displaySettingsBreadCrumb = wrapper.find(SetPageChrome).at(4); - - expect(schemaBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]); - expect(schemaErrorsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]); - expect(displaySettingsBreadCrumb.prop('trail')).toEqual([ - ...loadingBreadcrumbs, - NAV.DISPLAY_SETTINGS, - ]); - }); - - describe('reset state', () => { - it('resets state when leaving source tree', () => { - mockLocation.pathname = '/home'; - shallow(); - unmountHandler(); - - expect(resetSourceState).toHaveBeenCalled(); - }); - }); }); 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 d5d6c8e541e4f..bf68a60757c0d 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 @@ -10,18 +10,11 @@ import React, { useEffect } from 'react'; import { Route, Switch, useLocation, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import moment from 'moment'; -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 { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; import { CUSTOM_SERVICE_TYPE } from '../../constants'; import { - ENT_SEARCH_LICENSE_MANAGEMENT, REINDEX_JOB_PATH, SOURCE_DETAILS_PATH, SOURCE_CONTENT_PATH, @@ -37,13 +30,7 @@ import { Overview } from './components/overview'; import { Schema } from './components/schema'; 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 = () => { @@ -61,84 +48,43 @@ export const SourceRouter: React.FC = () => { return resetSourceState; }, []); - if (dataLoading) return ; + if (dataLoading) { + return isOrganization ? ( + + ) : ( + + ); + } - const { - name, - createdAt, - serviceType, - serviceName, - isFederatedSource, - supportedByLicense, - } = contentSource; + const { serviceType } = contentSource; const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; - const pageHeader = ( - <> - - - - ); - - const callout = ( - <> - -

{SOURCE_DISABLED_CALLOUT_DESCRIPTION}

- - {SOURCE_DISABLED_CALLOUT_BUTTON} - -
- - - ); - return ( - <> - {!supportedByLicense && callout} - {pageHeader} - - - - - + + + + + + + + {isCustomSource && ( + + - - - - + )} + {isCustomSource && ( + + - {isCustomSource && ( - - - - - - )} - {isCustomSource && ( - - - - - - )} - {isCustomSource && ( - - - - - - )} - - - - + )} + {isCustomSource && ( + + - - + )} + + + + ); }; 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 84bff65e62cef..2abdba07b5c88 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 @@ -11,12 +11,8 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LicensingLogic } from '../../../shared/licensing'; -import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; -import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, @@ -52,71 +48,53 @@ export const SourcesRouter: React.FC = () => { }, [pathname]); return ( - <> - - - - - - + + + + + + + + {staticSourceData.map(({ addPath, accountContextOnly }, i) => ( + + {!hasPlatinumLicense && accountContextOnly ? ( + + ) : ( + + )} - - - - + ))} + {staticSourceData.map(({ addPath }, i) => ( + + - {staticSourceData.map(({ addPath, accountContextOnly, name }, i) => ( - - - {!hasPlatinumLicense && accountContextOnly ? ( - - ) : ( - - )} - - ))} - {staticSourceData.map(({ addPath, name }, i) => ( - - - - - ))} - {staticSourceData.map(({ addPath, name }, i) => ( - - - - - ))} - {staticSourceData.map(({ addPath, name, configuration: { needsConfiguration } }, i) => { - if (needsConfiguration) - return ( - - - - - ); - })} - {canCreatePersonalSources ? ( - - - - - - ) : ( - - )} - - - + ))} + {staticSourceData.map(({ addPath }, i) => ( + + - - + ))} + {staticSourceData.map(({ addPath, configuration: { needsConfiguration } }, i) => { + if (needsConfiguration) + return ( + + + + ); + })} + {canCreatePersonalSources ? ( + + - - + ) : ( + + )} + + + + + + + ); }; 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 cf23470e8155e..7bd40d6f04a56 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 @@ -25,6 +25,13 @@ describe('Overview', () => { expect(mockActions.initializeOverview).toHaveBeenCalled(); }); + it('does not render a page header when data is loading (to prevent a jump between non/onboarding headers)', () => { + setMockValues({ dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.prop('pageHeader')).toBeUndefined(); + }); + it('renders onboarding state', () => { setMockValues({ dataLoading: false }); const wrapper = shallow(); 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 0049c5b732d3d..c51fdb64b8f26 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 @@ -53,17 +53,15 @@ export const Overview: React.FC = () => { const hideOnboarding = hasUsers && hasOrgSources && isOldAccount && orgName !== defaultOrgName; - const headerTitle = dataLoading || hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE; - const headerDescription = - dataLoading || hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION; + const headerTitle = hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE; + const headerDescription = hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION; return ( 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 b32e3af021827..35619d2b2d560 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 @@ -40,6 +40,13 @@ describe('SourceConfig', () => { expect(wrapper.find(EuiConfirmModal)).toHaveLength(1); }); + it('renders a breadcrumb fallback while data is loading', () => { + setMockValues({ dataLoading: true, sourceConfigData: {} }); + const wrapper = shallow(); + + expect(wrapper.prop('pageChrome')).toEqual(['Settings', 'Content source connectors', '...']); + }); + it('handles delete click', () => { const wrapper = shallow(); const saveConfig = wrapper.find(SaveConfig); 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 f1dfda78ee13f..c2a0b60e1eca3 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 @@ -47,7 +47,7 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => { return ( Date: Tue, 22 Jun 2021 01:25:20 -0400 Subject: [PATCH 034/118] [Telemetry] Track event loop delays on the server (#101580) (#102847) Co-authored-by: Ahmad Bamieh --- .../collectors/event_loop_delays/constants.ts | 37 +++++ .../event_loop_delays.mocks.ts | 49 +++++++ .../event_loop_delays.test.ts | 135 ++++++++++++++++++ .../event_loop_delays/event_loop_delays.ts | 109 ++++++++++++++ .../event_loop_delays_usage_collector.test.ts | 84 +++++++++++ .../event_loop_delays_usage_collector.ts | 53 +++++++ .../collectors/event_loop_delays/index.ts | 11 ++ .../event_loop_delays/rollups/daily.test.ts | 81 +++++++++++ .../event_loop_delays/rollups/daily.ts | 35 +++++ .../event_loop_delays/rollups/index.ts | 9 ++ .../integration_tests/daily_rollups.test.ts | 94 ++++++++++++ .../event_loop_delays/saved_objects.test.ts | 122 ++++++++++++++++ .../event_loop_delays/saved_objects.ts | 72 ++++++++++ .../collectors/event_loop_delays/schema.ts | 111 ++++++++++++++ .../server/collectors/index.ts | 1 + .../server/plugin.test.ts | 5 +- .../kibana_usage_collection/server/plugin.ts | 33 +++-- src/plugins/telemetry/schema/oss_plugins.json | 87 +++++++++++ 18 files changed, 1114 insertions(+), 14 deletions(-) create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts new file mode 100644 index 0000000000000..1753c87c9d005 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/** + * Roll daily indices every 24h + */ +export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000; + +/** + * Start rolling indices after 5 minutes up + */ +export const ROLL_INDICES_START = 5 * 60 * 1000; + +/** + * Reset the event loop delay historgram every 1 hour + */ +export const MONITOR_EVENT_LOOP_DELAYS_INTERVAL = 1 * 60 * 60 * 1000; + +/** + * Reset the event loop delay historgram every 24h + */ +export const MONITOR_EVENT_LOOP_DELAYS_RESET = 24 * 60 * 60 * 1000; + +/** + * Start monitoring the event loop delays after 1 minute + */ +export const MONITOR_EVENT_LOOP_DELAYS_START = 1 * 60 * 1000; + +/** + * Event loop monitoring sampling rate in milliseconds. + */ +export const MONITOR_EVENT_LOOP_DELAYS_RESOLUTION = 10; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts new file mode 100644 index 0000000000000..6b03d3cc5cbd1 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import moment from 'moment'; +import type { IntervalHistogram } from './event_loop_delays'; + +export const mockMonitorEnable = jest.fn(); +export const mockMonitorPercentile = jest.fn(); +export const mockMonitorReset = jest.fn(); +export const mockMonitorDisable = jest.fn(); +export const monitorEventLoopDelay = jest.fn().mockReturnValue({ + enable: mockMonitorEnable, + percentile: mockMonitorPercentile, + disable: mockMonitorDisable, + reset: mockMonitorReset, +}); + +jest.doMock('perf_hooks', () => ({ + monitorEventLoopDelay, +})); + +function createMockHistogram(overwrites: Partial = {}): IntervalHistogram { + const now = moment(); + + return { + min: 9093120, + max: 53247999, + mean: 11993238.600747818, + exceeds: 0, + stddev: 1168191.9357543814, + fromTimestamp: now.startOf('day').toISOString(), + lastUpdatedAt: now.toISOString(), + percentiles: { + '50': 12607487, + '75': 12615679, + '95': 12648447, + '99': 12713983, + }, + ...overwrites, + }; +} + +export const mocked = { + createHistogram: createMockHistogram, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts new file mode 100644 index 0000000000000..d03236a9756b3 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Subject } from 'rxjs'; + +import { + mockMonitorEnable, + mockMonitorPercentile, + monitorEventLoopDelay, + mockMonitorReset, + mockMonitorDisable, +} from './event_loop_delays.mocks'; +import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import { startTrackingEventLoopDelaysUsage, EventLoopDelaysCollector } from './event_loop_delays'; + +describe('EventLoopDelaysCollector', () => { + jest.useFakeTimers('modern'); + const mockNow = jest.getRealSystemTime(); + jest.setSystemTime(mockNow); + + beforeEach(() => jest.clearAllMocks()); + afterAll(() => jest.useRealTimers()); + + test('#constructor enables monitoring', () => { + new EventLoopDelaysCollector(); + expect(monitorEventLoopDelay).toBeCalledWith({ resolution: 10 }); + expect(mockMonitorEnable).toBeCalledTimes(1); + }); + + test('#collect returns event loop delays histogram', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + const histogramData = eventLoopDelaysCollector.collect(); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(1, 50); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(2, 75); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(3, 95); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(4, 99); + + expect(Object.keys(histogramData)).toMatchInlineSnapshot(` + Array [ + "min", + "max", + "mean", + "exceeds", + "stddev", + "fromTimestamp", + "lastUpdatedAt", + "percentiles", + ] + `); + }); + test('#reset resets histogram data', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + eventLoopDelaysCollector.reset(); + expect(mockMonitorReset).toBeCalledTimes(1); + }); + test('#stop disables monitoring event loop delays', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + eventLoopDelaysCollector.stop(); + expect(mockMonitorDisable).toBeCalledTimes(1); + }); +}); + +describe('startTrackingEventLoopDelaysUsage', () => { + const mockInternalRepository = savedObjectsRepositoryMock.create(); + const stopMonitoringEventLoop$ = new Subject(); + + beforeAll(() => jest.useFakeTimers('modern')); + beforeEach(() => jest.clearAllMocks()); + afterEach(() => stopMonitoringEventLoop$.next()); + + it('initializes EventLoopDelaysCollector and starts timer', () => { + const collectionStartDelay = 1000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay + ); + + expect(monitorEventLoopDelay).toBeCalledTimes(1); + expect(mockMonitorPercentile).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionStartDelay); + expect(mockMonitorPercentile).toBeCalled(); + }); + + it('stores event loop delays every collectionInterval duration', () => { + const collectionStartDelay = 100; + const collectionInterval = 1000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay, + collectionInterval + ); + + expect(mockInternalRepository.create).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionStartDelay); + expect(mockInternalRepository.create).toBeCalledTimes(1); + jest.advanceTimersByTime(collectionInterval); + expect(mockInternalRepository.create).toBeCalledTimes(2); + jest.advanceTimersByTime(collectionInterval); + expect(mockInternalRepository.create).toBeCalledTimes(3); + }); + + it('resets histogram every histogramReset duration', () => { + const collectionStartDelay = 0; + const collectionInterval = 1000; + const histogramReset = 5000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay, + collectionInterval, + histogramReset + ); + + expect(mockMonitorReset).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionInterval * 5); + expect(mockMonitorReset).toBeCalledTimes(1); + jest.advanceTimersByTime(collectionInterval * 5); + expect(mockMonitorReset).toBeCalledTimes(2); + }); + + it('stops monitoring event loop delays once stopMonitoringEventLoop$.next is called', () => { + startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$); + + expect(mockMonitorDisable).toBeCalledTimes(0); + stopMonitoringEventLoop$.next(); + expect(mockMonitorDisable).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts new file mode 100644 index 0000000000000..655cba580fc5d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EventLoopDelayMonitor } from 'perf_hooks'; +import { monitorEventLoopDelay } from 'perf_hooks'; +import { takeUntil, finalize, map } from 'rxjs/operators'; +import { Observable, timer } from 'rxjs'; +import type { ISavedObjectsRepository } from 'kibana/server'; +import { + MONITOR_EVENT_LOOP_DELAYS_START, + MONITOR_EVENT_LOOP_DELAYS_INTERVAL, + MONITOR_EVENT_LOOP_DELAYS_RESET, + MONITOR_EVENT_LOOP_DELAYS_RESOLUTION, +} from './constants'; +import { storeHistogram } from './saved_objects'; + +export interface IntervalHistogram { + fromTimestamp: string; + lastUpdatedAt: string; + min: number; + max: number; + mean: number; + exceeds: number; + stddev: number; + percentiles: { + 50: number; + 75: number; + 95: number; + 99: number; + }; +} + +export class EventLoopDelaysCollector { + private readonly loopMonitor: EventLoopDelayMonitor; + private fromTimestamp: Date; + + constructor() { + const monitor = monitorEventLoopDelay({ + resolution: MONITOR_EVENT_LOOP_DELAYS_RESOLUTION, + }); + monitor.enable(); + this.fromTimestamp = new Date(); + this.loopMonitor = monitor; + } + + public collect(): IntervalHistogram { + const { min, max, mean, exceeds, stddev } = this.loopMonitor; + + return { + min, + max, + mean, + exceeds, + stddev, + fromTimestamp: this.fromTimestamp.toISOString(), + lastUpdatedAt: new Date().toISOString(), + percentiles: { + 50: this.loopMonitor.percentile(50), + 75: this.loopMonitor.percentile(75), + 95: this.loopMonitor.percentile(95), + 99: this.loopMonitor.percentile(99), + }, + }; + } + + public reset() { + this.loopMonitor.reset(); + this.fromTimestamp = new Date(); + } + + public stop() { + this.loopMonitor.disable(); + } +} + +/** + * The monitoring of the event loop starts immediately. + * The first collection of the histogram happens after 1 minute. + * The daily histogram data is updated every 1 hour. + */ +export function startTrackingEventLoopDelaysUsage( + internalRepository: ISavedObjectsRepository, + stopMonitoringEventLoop$: Observable, + collectionStartDelay = MONITOR_EVENT_LOOP_DELAYS_START, + collectionInterval = MONITOR_EVENT_LOOP_DELAYS_INTERVAL, + histogramReset = MONITOR_EVENT_LOOP_DELAYS_RESET +) { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + + const resetOnCount = Math.ceil(histogramReset / collectionInterval); + timer(collectionStartDelay, collectionInterval) + .pipe( + map((i) => (i + 1) % resetOnCount === 0), + takeUntil(stopMonitoringEventLoop$), + finalize(() => eventLoopDelaysCollector.stop()) + ) + .subscribe(async (shouldReset) => { + const histogram = eventLoopDelaysCollector.collect(); + if (shouldReset) { + eventLoopDelaysCollector.reset(); + } + await storeHistogram(histogram, internalRepository); + }); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts new file mode 100644 index 0000000000000..06c51f6afa3a8 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + Collector, + createUsageCollectionSetupMock, + createCollectorFetchContextMock, +} from '../../../../usage_collection/server/mocks'; +import { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector'; +import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../core/server'; + +const logger = loggingSystemMock.createLogger(); + +describe('registerEventLoopDelaysCollector', () => { + let collector: Collector; + const mockRegisterType = jest.fn(); + const mockInternalRepository = savedObjectsRepositoryMock.create(); + const mockGetSavedObjectsClient = () => mockInternalRepository; + + const usageCollectionMock = createUsageCollectionSetupMock(); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = new Collector(logger, config); + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + + const collectorFetchContext = createCollectorFetchContextMock(); + + beforeAll(() => { + registerEventLoopDelaysCollector( + logger, + usageCollectionMock, + mockRegisterType, + mockGetSavedObjectsClient + ); + }); + + it('registers event_loop_delays collector', () => { + expect(collector).not.toBeUndefined(); + expect(collector.type).toBe('event_loop_delays'); + }); + + it('registers savedObjectType "event_loop_delays_daily"', () => { + expect(mockRegisterType).toBeCalledTimes(1); + expect(mockRegisterType).toBeCalledWith( + expect.objectContaining({ + name: 'event_loop_delays_daily', + }) + ); + }); + + it('returns objects from event_loop_delays_daily from fetch function', async () => { + const mockFind = jest.fn().mockResolvedValue(({ + saved_objects: [{ attributes: { test: 1 } }], + } as unknown) as SavedObjectsFindResponse); + mockInternalRepository.find = mockFind; + const fetchResult = await collector.fetch(collectorFetchContext); + + expect(fetchResult).toMatchInlineSnapshot(` + Object { + "daily": Array [ + Object { + "test": 1, + }, + ], + } + `); + expect(mockFind).toBeCalledTimes(1); + expect(mockFind.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "sortField": "updated_at", + "sortOrder": "desc", + "type": "event_loop_delays_daily", + }, + ] + `); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts new file mode 100644 index 0000000000000..774e021d7a549 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { timer } from 'rxjs'; +import { SavedObjectsServiceSetup, ISavedObjectsRepository, Logger } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { rollDailyData } from './rollups'; +import { registerSavedObjectTypes, EventLoopDelaysDaily } from './saved_objects'; +import { eventLoopDelaysUsageSchema, EventLoopDelaysUsageReport } from './schema'; +import { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects'; +import { ROLL_DAILY_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; + +export function registerEventLoopDelaysCollector( + logger: Logger, + usageCollection: UsageCollectionSetup, + registerType: SavedObjectsServiceSetup['registerType'], + getSavedObjectsClient: () => ISavedObjectsRepository | undefined +) { + registerSavedObjectTypes(registerType); + + timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe(() => + rollDailyData(logger, getSavedObjectsClient()) + ); + + const collector = usageCollection.makeUsageCollector({ + type: 'event_loop_delays', + isReady: () => typeof getSavedObjectsClient() !== 'undefined', + schema: eventLoopDelaysUsageSchema, + fetch: async () => { + const internalRepository = getSavedObjectsClient(); + if (!internalRepository) { + return { daily: [] }; + } + + const { saved_objects: savedObjects } = await internalRepository.find({ + type: SAVED_OBJECTS_DAILY_TYPE, + sortField: 'updated_at', + sortOrder: 'desc', + }); + + return { + daily: savedObjects.map((savedObject) => savedObject.attributes), + }; + }, + }); + + usageCollection.registerCollector(collector); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts new file mode 100644 index 0000000000000..693b173c2759e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { startTrackingEventLoopDelaysUsage } from './event_loop_delays'; +export { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector'; +export { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts new file mode 100644 index 0000000000000..cb59d6a44b07e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { rollDailyData } from './daily'; +import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../../core/server'; + +describe('rollDailyData', () => { + const logger = loggingSystemMock.createLogger(); + const mockSavedObjectsClient = savedObjectsRepositoryMock.create(); + + beforeEach(() => jest.clearAllMocks()); + + it('returns false if no savedObjectsClient', async () => { + await rollDailyData(logger, undefined); + expect(mockSavedObjectsClient.find).toBeCalledTimes(0); + }); + + it('calls delete on documents older than 3 days', async () => { + mockSavedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }], + } as SavedObjectsFindResponse); + + await rollDailyData(logger, mockSavedObjectsClient); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.delete).toBeCalledTimes(2); + expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith( + 1, + 'event_loop_delays_daily', + 'test_id_1' + ); + expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith( + 2, + 'event_loop_delays_daily', + 'test_id_2' + ); + }); + + it('calls logger.debug on repository find error', async () => { + const mockError = new Error('find error'); + mockSavedObjectsClient.find.mockRejectedValueOnce(mockError); + + await rollDailyData(logger, mockSavedObjectsClient); + expect(logger.debug).toBeCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + 'Failed to rollup transactional to daily entries' + ); + expect(logger.debug).toHaveBeenNthCalledWith(2, mockError); + }); + + it('settles all deletes before logging failures', async () => { + const mockError1 = new Error('delete error 1'); + const mockError2 = new Error('delete error 2'); + mockSavedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }, { id: 'test_id_3' }], + } as SavedObjectsFindResponse); + + mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError1); + mockSavedObjectsClient.delete.mockResolvedValueOnce(true); + mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError2); + + await rollDailyData(logger, mockSavedObjectsClient); + expect(mockSavedObjectsClient.delete).toBeCalledTimes(3); + expect(logger.debug).toBeCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + 'Failed to rollup transactional to daily entries' + ); + expect(logger.debug).toHaveBeenNthCalledWith(2, [ + { reason: mockError1, status: 'rejected' }, + { reason: mockError2, status: 'rejected' }, + ]); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts new file mode 100644 index 0000000000000..29072335d272b --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Logger } from '@kbn/logging'; +import { ISavedObjectsRepository } from '../../../../../../core/server'; +import { deleteHistogramSavedObjects } from '../saved_objects'; + +/** + * daily rollup function. Deletes histogram saved objects older than 3 days + * @param logger + * @param savedObjectsClient + */ +export async function rollDailyData( + logger: Logger, + savedObjectsClient?: ISavedObjectsRepository +): Promise { + if (!savedObjectsClient) { + return; + } + try { + const settledDeletes = await deleteHistogramSavedObjects(savedObjectsClient); + const failedDeletes = settledDeletes.filter(({ status }) => status !== 'fulfilled'); + if (failedDeletes.length) { + throw failedDeletes; + } + } catch (err) { + logger.debug(`Failed to rollup transactional to daily entries`); + logger.debug(err); + } +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts new file mode 100644 index 0000000000000..4523069a820e7 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { rollDailyData } from './daily'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts new file mode 100644 index 0000000000000..8c227f260da6e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Logger, ISavedObjectsRepository } from '../../../../../../../core/server'; +import { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + createRootWithCorePlugins, +} from '../../../../../../../core/test_helpers/kbn_server'; +import { rollDailyData } from '../daily'; +import { mocked } from '../../event_loop_delays.mocks'; + +import { + SAVED_OBJECTS_DAILY_TYPE, + serializeSavedObjectId, + EventLoopDelaysDaily, +} from '../../saved_objects'; +import moment from 'moment'; + +const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); + +function createRawObject(date: moment.MomentInput) { + const pid = Math.round(Math.random() * 10000); + return { + type: SAVED_OBJECTS_DAILY_TYPE, + id: serializeSavedObjectId({ pid, date }), + attributes: { + ...mocked.createHistogram({ + fromTimestamp: moment(date).startOf('day').toISOString(), + lastUpdatedAt: moment(date).toISOString(), + }), + processId: pid, + }, + }; +} + +const rawEventLoopDelaysDaily = [ + createRawObject(moment.now()), + createRawObject(moment.now()), + createRawObject(moment().subtract(1, 'days')), + createRawObject(moment().subtract(3, 'days')), +]; + +const outdatedRawEventLoopDelaysDaily = [ + createRawObject(moment().subtract(5, 'days')), + createRawObject(moment().subtract(7, 'days')), +]; + +describe('daily rollups integration test', () => { + let esServer: TestElasticsearchUtils; + let root: TestKibanaUtils['root']; + let internalRepository: ISavedObjectsRepository; + let logger: Logger; + + beforeAll(async () => { + esServer = await startES(); + root = createRootWithCorePlugins(); + + await root.setup(); + const start = await root.start(); + logger = root.logger.get('test dailt rollups'); + internalRepository = start.savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]); + + await internalRepository.bulkCreate( + [...rawEventLoopDelaysDaily, ...outdatedRawEventLoopDelaysDaily], + { refresh: true } + ); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + it('deletes documents older that 3 days from the saved objects repository', async () => { + await rollDailyData(logger, internalRepository); + const { + total, + saved_objects: savedObjects, + } = await internalRepository.find({ type: SAVED_OBJECTS_DAILY_TYPE }); + expect(total).toBe(rawEventLoopDelaysDaily.length); + expect(savedObjects.map(({ id, type, attributes }) => ({ id, type, attributes }))).toEqual( + rawEventLoopDelaysDaily + ); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts new file mode 100644 index 0000000000000..022040615bd45 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + storeHistogram, + serializeSavedObjectId, + deleteHistogramSavedObjects, +} from './saved_objects'; +import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../core/server/'; +import { mocked } from './event_loop_delays.mocks'; + +describe('serializeSavedObjectId', () => { + it('returns serialized id', () => { + const id = serializeSavedObjectId({ date: 1623233091278, pid: 123 }); + expect(id).toBe('123::09062021'); + }); +}); + +describe('storeHistogram', () => { + const mockHistogram = mocked.createHistogram(); + const mockInternalRepository = savedObjectsRepositoryMock.create(); + + jest.useFakeTimers('modern'); + const mockNow = jest.getRealSystemTime(); + jest.setSystemTime(mockNow); + + beforeEach(() => jest.clearAllMocks()); + afterAll(() => jest.useRealTimers()); + + it('stores histogram data in a savedObject', async () => { + await storeHistogram(mockHistogram, mockInternalRepository); + const pid = process.pid; + const id = serializeSavedObjectId({ date: mockNow, pid }); + + expect(mockInternalRepository.create).toBeCalledWith( + 'event_loop_delays_daily', + { ...mockHistogram, processId: pid }, + { id, overwrite: true } + ); + }); +}); + +describe('deleteHistogramSavedObjects', () => { + const mockInternalRepository = savedObjectsRepositoryMock.create(); + + beforeEach(() => { + jest.clearAllMocks(); + mockInternalRepository.find.mockResolvedValue({ + saved_objects: [{ id: 'test_obj_1' }, { id: 'test_obj_1' }], + } as SavedObjectsFindResponse); + }); + + it('builds filter query based on time range passed in days', async () => { + await deleteHistogramSavedObjects(mockInternalRepository); + await deleteHistogramSavedObjects(mockInternalRepository, 20); + expect(mockInternalRepository.find.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-3d/d\\"", + "type": "event_loop_delays_daily", + }, + ], + Array [ + Object { + "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-20d/d\\"", + "type": "event_loop_delays_daily", + }, + ], + ] + `); + }); + + it('loops over saved objects and deletes them', async () => { + mockInternalRepository.delete.mockImplementation(async (type, id) => { + return id; + }); + + const results = await deleteHistogramSavedObjects(mockInternalRepository); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + ] + `); + }); + + it('settles all promises even if some of the deletes fail.', async () => { + mockInternalRepository.delete.mockImplementationOnce(async (type, id) => { + throw new Error('Intentional failure'); + }); + mockInternalRepository.delete.mockImplementationOnce(async (type, id) => { + return id; + }); + + const results = await deleteHistogramSavedObjects(mockInternalRepository); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "reason": [Error: Intentional failure], + "status": "rejected", + }, + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + ] + `); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts new file mode 100644 index 0000000000000..610a6697da364 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { + SavedObjectAttributes, + SavedObjectsServiceSetup, + ISavedObjectsRepository, +} from 'kibana/server'; +import moment from 'moment'; +import type { IntervalHistogram } from './event_loop_delays'; + +export const SAVED_OBJECTS_DAILY_TYPE = 'event_loop_delays_daily'; + +export interface EventLoopDelaysDaily extends SavedObjectAttributes, IntervalHistogram { + processId: number; +} + +export function registerSavedObjectTypes(registerType: SavedObjectsServiceSetup['registerType']) { + registerType({ + name: SAVED_OBJECTS_DAILY_TYPE, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: { + // This type requires `lastUpdatedAt` to be indexed so we can use it when rolling up totals (lastUpdatedAt < now-90d) + lastUpdatedAt: { type: 'date' }, + }, + }, + }); +} + +export function serializeSavedObjectId({ date, pid }: { date: moment.MomentInput; pid: number }) { + const formattedDate = moment(date).format('DDMMYYYY'); + + return `${pid}::${formattedDate}`; +} + +export async function deleteHistogramSavedObjects( + internalRepository: ISavedObjectsRepository, + daysTimeRange = 3 +) { + const { saved_objects: savedObjects } = await internalRepository.find({ + type: SAVED_OBJECTS_DAILY_TYPE, + filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.lastUpdatedAt < "now-${daysTimeRange}d/d"`, + }); + + return await Promise.allSettled( + savedObjects.map(async (savedObject) => { + return await internalRepository.delete(SAVED_OBJECTS_DAILY_TYPE, savedObject.id); + }) + ); +} + +export async function storeHistogram( + histogram: IntervalHistogram, + internalRepository: ISavedObjectsRepository +) { + const pid = process.pid; + const id = serializeSavedObjectId({ date: histogram.lastUpdatedAt, pid }); + + return await internalRepository.create( + SAVED_OBJECTS_DAILY_TYPE, + { ...histogram, processId: pid }, + { id, overwrite: true } + ); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts new file mode 100644 index 0000000000000..319e8c77438b8 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; + +export interface EventLoopDelaysUsageReport { + daily: Array<{ + processId: number; + lastUpdatedAt: string; + fromTimestamp: string; + min: number; + max: number; + mean: number; + exceeds: number; + stddev: number; + percentiles: { + '50': number; + '75': number; + '95': number; + '99': number; + }; + }>; +} + +export const eventLoopDelaysUsageSchema: MakeSchemaFrom = { + daily: { + type: 'array', + items: { + processId: { + type: 'long', + _meta: { + description: 'The process id of the monitored kibana instance.', + }, + }, + fromTimestamp: { + type: 'date', + _meta: { + description: 'Timestamp at which the histogram started monitoring.', + }, + }, + lastUpdatedAt: { + type: 'date', + _meta: { + description: 'Latest timestamp this histogram object was updated this day.', + }, + }, + min: { + type: 'long', + _meta: { + description: 'The minimum recorded event loop delay.', + }, + }, + max: { + type: 'long', + _meta: { + description: 'The maximum recorded event loop delay.', + }, + }, + mean: { + type: 'long', + _meta: { + description: 'The mean of the recorded event loop delays.', + }, + }, + exceeds: { + type: 'long', + _meta: { + description: + 'The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold.', + }, + }, + stddev: { + type: 'long', + _meta: { + description: 'The standard deviation of the recorded event loop delays.', + }, + }, + percentiles: { + '50': { + type: 'long', + _meta: { + description: 'The 50th accumulated percentile distribution', + }, + }, + '75': { + type: 'long', + _meta: { + description: 'The 75th accumulated percentile distribution', + }, + }, + '95': { + type: 'long', + _meta: { + description: 'The 95th accumulated percentile distribution', + }, + }, + '99': { + type: 'long', + _meta: { + description: 'The 99th accumulated percentile distribution', + }, + }, + }, + }, + }, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 761989938e56d..e4ed24611bfa8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -28,3 +28,4 @@ export { registerUsageCountersRollups, registerUsageCountersUsageCollector, } from './usage_counters'; +export { registerEventLoopDelaysCollector } from './event_loop_delays'; diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts index 2100b9bbb918b..1584366a42dc1 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts @@ -16,7 +16,6 @@ import { createUsageCollectionSetupMock, } from '../../usage_collection/server/mocks'; import { cloudDetailsMock } from './mocks'; - import { plugin } from './'; describe('kibana_usage_collection', () => { @@ -105,6 +104,10 @@ describe('kibana_usage_collection', () => { "isReady": true, "type": "localization", }, + Object { + "isReady": false, + "type": "event_loop_delays", + }, ] `); }); diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index da6445ce957d8..4ec717c48610e 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -22,6 +22,10 @@ import type { CoreUsageDataStart, } from 'src/core/server'; import { SavedObjectsClient } from '../../../core/server'; +import { + startTrackingEventLoopDelaysUsage, + SAVED_OBJECTS_DAILY_TYPE, +} from './collectors/event_loop_delays'; import { registerApplicationUsageCollector, registerKibanaUsageCollector, @@ -39,6 +43,7 @@ import { registerUsageCountersRollups, registerUsageCountersUsageCollector, registerSavedObjectsCountUsageCollector, + registerEventLoopDelaysCollector, } from './collectors'; interface KibanaUsageCollectionPluginsDepsSetup { @@ -54,46 +59,46 @@ export class KibanaUsageCollectionPlugin implements Plugin { private uiSettingsClient?: IUiSettingsClient; private metric$: Subject; private coreUsageData?: CoreUsageDataStart; - private stopUsingUiCounterIndicies$: Subject; + private pluginStop$: Subject; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; this.metric$ = new Subject(); - this.stopUsingUiCounterIndicies$ = new Subject(); + this.pluginStop$ = new Subject(); } public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { usageCollection.createUsageCounter('uiCounters'); - this.registerUsageCollectors( usageCollection, coreSetup, this.metric$, - this.stopUsingUiCounterIndicies$, + this.pluginStop$, coreSetup.savedObjects.registerType.bind(coreSetup.savedObjects) ); } public start(core: CoreStart) { const { savedObjects, uiSettings } = core; - this.savedObjectsClient = savedObjects.createInternalRepository(); + this.savedObjectsClient = savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); core.metrics.getOpsMetrics$().subscribe(this.metric$); this.coreUsageData = core.coreUsageData; + startTrackingEventLoopDelaysUsage(this.savedObjectsClient, this.pluginStop$.asObservable()); } public stop() { this.metric$.complete(); - this.stopUsingUiCounterIndicies$.complete(); + this.pluginStop$.complete(); } private registerUsageCollectors( usageCollection: UsageCollectionSetup, coreSetup: CoreSetup, metric$: Subject, - stopUsingUiCounterIndicies$: Subject, + pluginStop$: Subject, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; @@ -101,12 +106,8 @@ export class KibanaUsageCollectionPlugin implements Plugin { const getCoreUsageDataService = () => this.coreUsageData!; registerUiCounterSavedObjectType(coreSetup.savedObjects); - registerUiCountersRollups( - this.logger.get('ui-counters'), - stopUsingUiCounterIndicies$, - getSavedObjectsClient - ); - registerUiCountersUsageCollector(usageCollection, stopUsingUiCounterIndicies$); + registerUiCountersRollups(this.logger.get('ui-counters'), pluginStop$, getSavedObjectsClient); + registerUiCountersUsageCollector(usageCollection, pluginStop$); registerUsageCountersRollups(this.logger.get('usage-counters-rollup'), getSavedObjectsClient); registerUsageCountersUsageCollector(usageCollection); @@ -127,5 +128,11 @@ export class KibanaUsageCollectionPlugin implements Plugin { registerCoreUsageCollector(usageCollection, getCoreUsageDataService); registerConfigUsageCollector(usageCollection, getCoreUsageDataService); registerLocalizationUsageCollector(usageCollection, coreSetup.i18n); + registerEventLoopDelaysCollector( + this.logger.get('event-loop-delays'), + usageCollection, + registerType, + getSavedObjectsClient + ); } } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 0abaeebfc7723..6a875998d5a9e 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7900,6 +7900,93 @@ } } }, + "event_loop_delays": { + "properties": { + "daily": { + "type": "array", + "items": { + "properties": { + "processId": { + "type": "long", + "_meta": { + "description": "The process id of the monitored kibana instance." + } + }, + "fromTimestamp": { + "type": "date", + "_meta": { + "description": "Timestamp at which the histogram started monitoring." + } + }, + "lastUpdatedAt": { + "type": "date", + "_meta": { + "description": "Latest timestamp this histogram object was updated this day." + } + }, + "min": { + "type": "long", + "_meta": { + "description": "The minimum recorded event loop delay." + } + }, + "max": { + "type": "long", + "_meta": { + "description": "The maximum recorded event loop delay." + } + }, + "mean": { + "type": "long", + "_meta": { + "description": "The mean of the recorded event loop delays." + } + }, + "exceeds": { + "type": "long", + "_meta": { + "description": "The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold." + } + }, + "stddev": { + "type": "long", + "_meta": { + "description": "The standard deviation of the recorded event loop delays." + } + }, + "percentiles": { + "properties": { + "50": { + "type": "long", + "_meta": { + "description": "The 50th accumulated percentile distribution" + } + }, + "75": { + "type": "long", + "_meta": { + "description": "The 75th accumulated percentile distribution" + } + }, + "95": { + "type": "long", + "_meta": { + "description": "The 95th accumulated percentile distribution" + } + }, + "99": { + "type": "long", + "_meta": { + "description": "The 99th accumulated percentile distribution" + } + } + } + } + } + } + } + } + }, "localization": { "properties": { "locale": { From f252a9aa75098d72e3789a2f649823b0d771abe1 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 22 Jun 2021 10:38:25 +0300 Subject: [PATCH 035/118] [Lens] Adds filter from legend in xy and partition charts (#102026) (#102852) * WIP add filtering capabilities to XY legend * Fix filter by legend on xy axis charts * Filter pie and xy axis by legend * create a shared component * Add functional test * Add functional test for pie * Make the buttons keyboard accessible * Fix functional test * move function to retry * Give another try * Enable the rest od the tests * Address PR comments * Address PR comments * Apply PR comments, fix popover label for alreadyformatted layers Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../get_legend_action.test.tsx | 79 ++++++ .../pie_visualization/get_legend_action.tsx | 44 ++++ .../pie_visualization/render_function.tsx | 2 + .../lens/public/shared_components/index.ts | 1 + .../legend_action_popover.tsx | 102 ++++++++ .../__snapshots__/expression.test.tsx.snap | 49 ++++ .../public/xy_visualization/expression.tsx | 9 + .../get_legend_action.test.tsx | 232 ++++++++++++++++++ .../xy_visualization/get_legend_action.tsx | 72 ++++++ x-pack/test/functional/apps/lens/dashboard.ts | 2 +- .../test/functional/apps/lens/smokescreen.ts | 62 +++++ .../test/functional/page_objects/lens_page.ts | 6 + 12 files changed, 659 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx create mode 100644 x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx create mode 100644 x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx new file mode 100644 index 0000000000000..67e57dadd4935 --- /dev/null +++ b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 { LegendActionProps, SeriesIdentifier } from '@elastic/charts'; +import { EuiPopover } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ComponentType, ReactWrapper } from 'enzyme'; +import type { Datatable } from 'src/plugins/expressions/public'; +import { getLegendAction } from './get_legend_action'; +import { LegendActionPopover } from '../shared_components'; + +const table: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, + ], + rows: [ + { a: 'Hi', b: 2 }, + { a: 'Test', b: 4 }, + { a: 'Foo', b: 6 }, + ], +}; + +describe('getLegendAction', function () { + let wrapperProps: LegendActionProps; + const Component: ComponentType = getLegendAction(table, jest.fn()); + let wrapper: ReactWrapper; + + beforeAll(() => { + wrapperProps = { + color: 'rgb(109, 204, 177)', + label: 'Bar', + series: ([ + { + specId: 'donut', + key: 'Bar', + }, + ] as unknown) as SeriesIdentifier[], + }; + }); + + it('is not rendered if row does not exist', () => { + wrapper = mountWithIntl(); + expect(wrapper).toEqual({}); + expect(wrapper.find(EuiPopover).length).toBe(0); + }); + + it('is rendered if row is detected', () => { + const newProps = { + ...wrapperProps, + label: 'Hi', + series: ([ + { + specId: 'donut', + key: 'Hi', + }, + ] as unknown) as SeriesIdentifier[], + }; + wrapper = mountWithIntl(); + expect(wrapper.find(EuiPopover).length).toBe(1); + expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options'); + expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({ + data: [ + { + column: 0, + row: 0, + table, + value: 'Hi', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx new file mode 100644 index 0000000000000..9f16ad863a415 --- /dev/null +++ b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx @@ -0,0 +1,44 @@ +/* + * 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 type { LegendAction } from '@elastic/charts'; +import type { Datatable } from 'src/plugins/expressions/public'; +import type { LensFilterEvent } from '../types'; +import { LegendActionPopover } from '../shared_components'; + +export const getLegendAction = ( + table: Datatable, + onFilter: (data: LensFilterEvent['data']) => void +): LegendAction => + React.memo(({ series: [pieSeries], label }) => { + const data = table.columns.reduce((acc, { id }, column) => { + const value = pieSeries.key; + const row = table.rows.findIndex((r) => r[id] === value); + if (row > -1) { + acc.push({ + table, + column, + row, + value, + }); + } + + return acc; + }, []); + + if (data.length === 0) { + return null; + } + + const context: LensFilterEvent['data'] = { + data, + }; + + return ; + }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 6c1cbe63a5a3e..f329cfe1bb8b9 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -38,6 +38,7 @@ import { SeriesLayer, } from '../../../../../src/plugins/charts/public'; import { LensIconChartDonut } from '../assets/chart_donut'; +import { getLegendAction } from './get_legend_action'; declare global { interface Window { @@ -281,6 +282,7 @@ export function PieComponent( onElementClick={ props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined } + legendAction={getLegendAction(firstTable, onClickValue)} theme={{ ...chartTheme, background: { diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index cf8536884acdf..c200a18a25caf 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -13,3 +13,4 @@ export { TooltipWrapper } from './tooltip_wrapper'; export * from './coloring'; export { useDebouncedValue } from './debounced_value'; export * from './helpers'; +export { LegendActionPopover } from './legend_action_popover'; diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx new file mode 100644 index 0000000000000..e344cb5289f51 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.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 { i18n } from '@kbn/i18n'; +import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import type { LensFilterEvent } from '../types'; +import { desanitizeFilterContext } from '../utils'; + +export interface LegendActionPopoverProps { + /** + * Determines the panels label + */ + label: string; + /** + * Callback on filter value + */ + onFilter: (data: LensFilterEvent['data']) => void; + /** + * Determines the filter event data + */ + context: LensFilterEvent['data']; +} + +export const LegendActionPopover: React.FunctionComponent = ({ + label, + onFilter, + context, +}) => { + const [popoverOpen, setPopoverOpen] = useState(false); + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 'main', + title: label, + items: [ + { + name: i18n.translate('xpack.lens.shared.legend.filterForValueButtonAriaLabel', { + defaultMessage: 'Filter for value', + }), + 'data-test-subj': `legend-${label}-filterIn`, + icon: , + onClick: () => { + setPopoverOpen(false); + onFilter(desanitizeFilterContext(context)); + }, + }, + { + name: i18n.translate('xpack.lens.shared.legend.filterOutValueButtonAriaLabel', { + defaultMessage: 'Filter out value', + }), + 'data-test-subj': `legend-${label}-filterOut`, + icon: , + onClick: () => { + setPopoverOpen(false); + onFilter(desanitizeFilterContext({ ...context, negate: true })); + }, + }, + ], + }, + ]; + + const Button = ( +
setPopoverOpen(!popoverOpen)} + onClick={() => setPopoverOpen(!popoverOpen)} + > + +
+ ); + return ( + setPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="upLeft" + title={i18n.translate('xpack.lens.shared.legend.filterOptionsLegend', { + defaultMessage: '{legendDataLabel}, filter options', + values: { legendDataLabel: label }, + })} + > + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index f9b4e33072c81..1f647680408d7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -7,6 +7,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` = {}; + // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers const safeXAccessorLabelRenderer = (value: unknown): string => xAxisColumn && layersAlreadyFormatted[xAxisColumn.id] @@ -629,6 +631,13 @@ export function XYChart({ xDomain={xDomain} onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined} onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined} + legendAction={getLegendAction( + filteredLayers, + data.tables, + onClickValue, + formatFactory, + layersAlreadyFormatted + )} showLegendExtra={isHistogramViz && valuesInLegend} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx new file mode 100644 index 0000000000000..e4edfe918a242 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx @@ -0,0 +1,232 @@ +/* + * 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 { LegendActionProps, SeriesIdentifier } from '@elastic/charts'; +import { EuiPopover } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ComponentType, ReactWrapper } from 'enzyme'; +import type { LayerArgs } from './types'; +import type { LensMultiTable } from '../types'; +import { getLegendAction } from './get_legend_action'; +import { LegendActionPopover } from '../shared_components'; + +const sampleLayer = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'splitAccessorId', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, +} as LayerArgs; + +const tables = { + first: { + type: 'datatable', + rows: [ + { + xAccessorId: 1585758120000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Accessories", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760700000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585761120000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'order_date per minute', + meta: { + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }, + params: { id: 'date', params: { pattern: 'HH:mm' } }, + }, + }, + { + id: 'splitAccessorId', + name: 'Top values of category.keyword', + meta: { + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, + }, + }, + }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + params: {}, + }, + params: { id: 'number' }, + }, + }, + ], + }, +} as LensMultiTable['tables']; + +describe('getLegendAction', function () { + let wrapperProps: LegendActionProps; + const Component: ComponentType = getLegendAction( + [sampleLayer], + tables, + jest.fn(), + jest.fn(), + {} + ); + let wrapper: ReactWrapper; + + beforeAll(() => { + wrapperProps = { + color: 'rgb(109, 204, 177)', + label: "Women's Accessories", + series: ([ + { + seriesKeys: ["Women's Accessories", 'test'], + }, + ] as unknown) as SeriesIdentifier[], + }; + }); + + it('is not rendered if not layer is detected', () => { + wrapper = mountWithIntl(); + expect(wrapper).toEqual({}); + expect(wrapper.find(EuiPopover).length).toBe(0); + }); + + it('is rendered if row does not exist', () => { + const newProps = { + ...wrapperProps, + series: ([ + { + seriesKeys: ['test', 'b'], + }, + ] as unknown) as SeriesIdentifier[], + }; + wrapper = mountWithIntl(); + expect(wrapper).toEqual({}); + expect(wrapper.find(EuiPopover).length).toBe(0); + }); + + it('is rendered if layer is detected', () => { + const newProps = { + ...wrapperProps, + series: ([ + { + seriesKeys: ["Women's Accessories", 'b'], + }, + ] as unknown) as SeriesIdentifier[], + }; + wrapper = mountWithIntl(); + expect(wrapper.find(EuiPopover).length).toBe(1); + expect(wrapper.find(EuiPopover).prop('title')).toEqual("Women's Accessories, filter options"); + expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({ + data: [ + { + column: 1, + row: 1, + table: tables.first, + value: "Women's Accessories", + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx new file mode 100644 index 0000000000000..c99bf948d6e37 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx @@ -0,0 +1,72 @@ +/* + * 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 type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts'; +import type { LayerArgs } from './types'; +import type { LensMultiTable, LensFilterEvent, FormatFactory } from '../types'; +import { LegendActionPopover } from '../shared_components'; + +export const getLegendAction = ( + filteredLayers: LayerArgs[], + tables: LensMultiTable['tables'], + onFilter: (data: LensFilterEvent['data']) => void, + formatFactory: FormatFactory, + layersAlreadyFormatted: Record +): LegendAction => + React.memo(({ series: [xySeries] }) => { + const series = xySeries as XYChartSeriesIdentifier; + const layer = filteredLayers.find((l) => + series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString())) + ); + + if (!layer || !layer.splitAccessor) { + return null; + } + + const splitLabel = series.seriesKeys[0] as string; + const accessor = layer.splitAccessor; + + const table = tables[layer.layerId]; + const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor); + const formatter = formatFactory(splitColumn && splitColumn.meta?.params); + + const rowIndex = table.rows.findIndex((row) => { + if (layersAlreadyFormatted[accessor]) { + // stringify the value to compare with the chart value + return formatter.convert(row[accessor]) === splitLabel; + } + return row[accessor] === splitLabel; + }); + + if (rowIndex < 0) return null; + + const data = [ + { + row: rowIndex, + column: table.columns.findIndex((col) => col.id === accessor), + value: accessor ? table.rows[rowIndex][accessor] : splitLabel, + table, + }, + ]; + + const context: LensFilterEvent['data'] = { + data, + }; + + return ( + + ); + }); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 9998f1dd4cdcb..844b074e42e74 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -68,7 +68,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await find.clickByButtonText('lnsXYvis'); await dashboardAddPanel.closeAddPanel(); await PageObjects.lens.goToTimeRange(); - await clickInChart(5, 5); // hardcoded position of bar, depends heavy on data and charts implementation + await clickInChart(6, 5); // hardcoded position of bar, depends heavy on data and charts implementation await retry.try(async () => { await testSubjects.click('applyFiltersPopoverButton'); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 5d775f154c943..ec32d7620fcf9 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); const elasticChart = getService('elasticChart'); + const filterBar = getService('filterBar'); describe('lens smokescreen tests', () => { it('should allow creation of lens xy chart', async () => { @@ -686,5 +687,66 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await testSubjects.isEnabled('lnsApp_downloadCSVButton')).to.eql(true); }); + + it('should allow filtering by legend on an xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'extension.raw', + }); + + await PageObjects.lens.filterLegend('jpg'); + const hasExtensionFilter = await filterBar.hasFilter('extension.raw', 'jpg'); + expect(hasExtensionFilter).to.be(true); + + await filterBar.removeFilter('extension.raw'); + }); + + it('should allow filtering by legend on a pie chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('pie'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'extension.raw', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'agent.raw', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.filterLegend('jpg'); + const hasExtensionFilter = await filterBar.hasFilter('extension.raw', 'jpg'); + expect(hasExtensionFilter).to.be(true); + + await filterBar.removeFilter('extension.raw'); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 332a40795bee9..c75bb56f6767d 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -1069,5 +1069,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await input.clearValueWithKeyboard(); await input.type(formula); }, + + async filterLegend(value: string) { + await testSubjects.click(`legend-${value}`); + const filterIn = await testSubjects.find(`legend-${value}-filterIn`); + await filterIn.click(); + }, }); } From db992d9da28a73192f16fe538f050f55e6602ba7 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Tue, 22 Jun 2021 11:47:27 +0200 Subject: [PATCH 036/118] [License management] Migrate to new page layout (#102218) (#102856) * start working on license management * migrate permissions check to new layout * refactor license expiration as a subtitle of the page header * finish up working on page title * Fix linter errors and update snapshots * update method name * CR changes * update snapshots Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/add_license.test.js.snap | 4 +- .../license_page_header.test.js.snap | 5 + .../__snapshots__/license_status.test.js.snap | 5 - .../request_trial_extension.test.js.snap | 8 +- .../revert_to_basic.test.js.snap | 6 +- .../__snapshots__/start_trial.test.js.snap | 8 +- .../upload_license.test.tsx.snap | 20 +- ...us.test.js => license_page_header.test.js} | 6 +- x-pack/plugins/license_management/kibana.json | 1 + .../public/application/app.js | 63 +++---- .../add_license/add_license.js | 1 + .../license_dashboard/license_dashboard.js | 33 ++-- .../index.js | 2 +- .../license_page_header.js | 106 +++++++++++ .../license_status.container.js | 36 ---- .../license_status/license_status.js | 98 ---------- .../request_trial_extension.js | 1 + .../revert_to_basic/revert_to_basic.js | 1 + .../start_trial/start_trial.tsx | 2 + .../sections/upload_license/upload_license.js | 176 +++++++++--------- .../store/reducers/license_management.js | 32 ++++ .../public/shared_imports.ts | 8 + .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 24 files changed, 325 insertions(+), 305 deletions(-) create mode 100644 x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap delete mode 100644 x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap rename x-pack/plugins/license_management/__jest__/{license_status.test.js => license_page_header.test.js} (83%) rename x-pack/plugins/license_management/public/application/sections/license_dashboard/{license_status => license_page_header}/index.js (80%) create mode 100644 x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js delete mode 100644 x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js delete mode 100644 x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js create mode 100644 x-pack/plugins/license_management/public/shared_imports.ts diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap index 95921fa61233c..90a3eb98c64a1 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; -exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap new file mode 100644 index 0000000000000..047e311f3d325 --- /dev/null +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on

"`; + +exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap deleted file mode 100644 index 9bd1c878f8679..0000000000000 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LicenseStatus component should display display warning is expired 1`] = `"

Your Platinum license has expired

Your license expired on
"`; - -exports[`LicenseStatus component should display normally when license is active 1`] = `"

Your Gold license is active

Your license will expire on October 12, 2099 7:00 PM EST
"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index 4d8b653c4b10d..fda479f2888ce 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index be634a5b4f748..4fa45c4bec5ce 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 1cacadb824630..622bff86ead16 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9f89179d207e0..29ec3ddbfdc02 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -262,16 +262,18 @@ exports[`UploadLicense should display a modal when license requires acknowledgem uploadLicenseStatus={[Function]} >
@@ -1301,16 +1303,18 @@ exports[`UploadLicense should display an error when ES says license is expired 1 uploadLicenseStatus={[Function]} >
@@ -2031,16 +2035,18 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 uploadLicenseStatus={[Function]} >
@@ -2761,16 +2767,18 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] uploadLicenseStatus={[Function]} >
@@ -3491,16 +3499,18 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` uploadLicenseStatus={[Function]} >
diff --git a/x-pack/plugins/license_management/__jest__/license_status.test.js b/x-pack/plugins/license_management/__jest__/license_page_header.test.js similarity index 83% rename from x-pack/plugins/license_management/__jest__/license_status.test.js rename to x-pack/plugins/license_management/__jest__/license_page_header.test.js index 898667e13a1b3..56a71eb8d252e 100644 --- a/x-pack/plugins/license_management/__jest__/license_status.test.js +++ b/x-pack/plugins/license_management/__jest__/license_page_header.test.js @@ -5,7 +5,7 @@ * 2.0. */ -import { LicenseStatus } from '../public/application/sections/license_dashboard/license_status'; +import { LicensePageHeader } from '../public/application/sections/license_dashboard/license_page_header'; import { createMockLicense, getComponent } from './util'; describe('LicenseStatus component', () => { @@ -14,7 +14,7 @@ describe('LicenseStatus component', () => { { license: createMockLicense('gold'), }, - LicenseStatus + LicensePageHeader ); expect(rendered.html()).toMatchSnapshot(); }); @@ -23,7 +23,7 @@ describe('LicenseStatus component', () => { { license: createMockLicense('platinum', 0), }, - LicenseStatus + LicensePageHeader ); expect(rendered.html()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json index 1f925a453898e..be2e21c7eb41e 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -9,6 +9,7 @@ "extraPublicDirs": ["common/constants"], "requiredBundles": [ "telemetryManagementSection", + "esUiShared", "kibanaReact" ] } diff --git a/x-pack/plugins/license_management/public/application/app.js b/x-pack/plugins/license_management/public/application/app.js index 3bfa22dd72921..4b5a6144dbdc9 100644 --- a/x-pack/plugins/license_management/public/application/app.js +++ b/x-pack/plugins/license_management/public/application/app.js @@ -10,7 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { LicenseDashboard, UploadLicense } from './sections'; import { Switch, Route } from 'react-router-dom'; import { APP_PERMISSION } from '../../common/constants'; -import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui'; +import { SectionLoading } from '../shared_imports'; +import { EuiPageContent, EuiPageBody, EuiEmptyPrompt } from '@elastic/eui'; export class App extends Component { componentDidMount() { @@ -23,52 +24,50 @@ export class App extends Component { if (permissionsLoading) { return ( - } - body={ - - - - } - data-test-subj="sectionLoading" - /> + + + + + ); } if (permissionsError) { + const error = permissionsError?.data?.message; + return ( - - } - color="danger" - iconType="alert" - > - {permissionsError.data && permissionsError.data.message ? ( -
{permissionsError.data.message}
- ) : null} -
+ + + + + } + body={error ?

{error}

: null} + /> +
); } if (!hasPermission) { return ( - + +

-

+ } body={

@@ -82,7 +81,7 @@ export class App extends Component {

} /> -
+ ); } diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js index 4120b2280a7a6..90de14b167e52 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js @@ -18,6 +18,7 @@ export const AddLicense = ({ uploadPath = `/upload_license` }) => { return ( {} }) => { useEffect(() => { @@ -19,17 +20,19 @@ export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb: }); return ( -
- - - - - - - - - - -
+ <> + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js similarity index 80% rename from x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js index efd4da2770db4..303e30040ab50 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js @@ -5,4 +5,4 @@ * 2.0. */ -export { LicenseStatus } from './license_status.container'; +export { LicensePageHeader } from './license_page_header'; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js new file mode 100644 index 0000000000000..df41d46ac5789 --- /dev/null +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js @@ -0,0 +1,106 @@ +/* + * 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 { useSelector } from 'react-redux'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; + +import { getLicenseState } from '../../../store/reducers/license_management'; + +export const ActiveLicensePageHeader = ({ license, ...props }) => { + return ( + + + + } + description={ + + {license.expirationDate ? ( + {license.expirationDate}, + }} + /> + ) : ( + + )} + + } + /> + ); +}; + +export const ExpiredLicensePageHeader = ({ license, ...props }) => { + return ( + + + + } + description={ + + {license.expirationDate}, + }} + /> + + } + /> + ); +}; + +export const LicensePageHeader = () => { + const license = useSelector(getLicenseState); + + return ( + <> + {license.isExpired ? ( + + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js deleted file mode 100644 index 01577e79fd6ec..0000000000000 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LicenseStatus as PresentationComponent } from './license_status'; -import { connect } from 'react-redux'; -import { - getLicense, - getExpirationDateFormatted, - isExpired, -} from '../../../store/reducers/license_management'; -import { i18n } from '@kbn/i18n'; - -const mapStateToProps = (state) => { - const { isActive, type } = getLicense(state); - return { - status: isActive - ? i18n.translate('xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText', { - defaultMessage: 'Active', - }) - : i18n.translate( - 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText', - { - defaultMessage: 'Inactive', - } - ), - type, - isExpired: isExpired(state), - expiryDate: getExpirationDateFormatted(state), - }; -}; - -export const LicenseStatus = connect(mapStateToProps)(PresentationComponent); diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js deleted file mode 100644 index 5f7e59bf1ceba..0000000000000 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js +++ /dev/null @@ -1,98 +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, { Fragment } from 'react'; - -import { - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTitle, - EuiSpacer, - EuiTextAlign, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class LicenseStatus extends React.PureComponent { - render() { - const { isExpired, status, type, expiryDate } = this.props; - const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); - let icon; - let title; - let message; - if (isExpired) { - icon = ; - message = ( - - {expiryDate}, - }} - /> - - ); - title = ( - - ); - } else { - icon = ; - message = expiryDate ? ( - - {expiryDate}, - }} - /> - - ) : ( - - - - ); - title = ( - - ); - } - return ( - - - {icon} - - -

{title}

-
-
-
- - - - {message} -
- ); - } -} diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js index 8c694cf27765a..e578c372b9c9f 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js @@ -37,6 +37,7 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => { return ( {this.acknowledgeModal()} { {this.acknowledgeModal(dependencies!.docLinks)} - - - -

- -

-
+ + + +

+ +

+
- + - {this.acknowledgeModal()} + {this.acknowledgeModal()} - -

- -

-

- {currentLicenseType.toUpperCase()}, - }} - /> -

-
- - - - - - - } - onChange={this.handleFile} + +

+ +

+

+ {currentLicenseType.toUpperCase()}, + }} + /> +

+
+ + + + + + + } + onChange={this.handleFile} + /> + + + + + {shouldShowTelemetryOptIn(telemetry) && ( + + )} + + + + + + + + + + {applying ? ( + -
-
-
- - {shouldShowTelemetryOptIn(telemetry) && ( - - )} - - - - + ) : ( - - - - - {applying ? ( - - ) : ( - - )} - - - -
-
-
- + )} + +
+ + +
+ ); } } diff --git a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js index 20e31cf89da72..1a985cd8ee623 100644 --- a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js +++ b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js @@ -6,6 +6,10 @@ */ import { combineReducers } from 'redux'; +import { i18n } from '@kbn/i18n'; +import { capitalize } from 'lodash'; +import { createSelector } from 'reselect'; + import { license } from './license'; import { uploadStatus } from './upload_status'; import { startBasicStatus } from './start_basic_license_status'; @@ -135,3 +139,31 @@ export const startBasicLicenseNeedsAcknowledgement = (state) => { export const getStartBasicMessages = (state) => { return state.startBasicStatus.messages; }; + +export const getLicenseState = createSelector( + getLicense, + getExpirationDateFormatted, + isExpired, + (license, expirationDate, isExpired) => { + const { isActive, type } = license; + + return { + type: capitalize(type), + isExpired, + expirationDate, + status: isActive + ? i18n.translate( + 'xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText', + { + defaultMessage: 'active', + } + ) + : i18n.translate( + 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText', + { + defaultMessage: 'inactive', + } + ), + }; + } +); diff --git a/x-pack/plugins/license_management/public/shared_imports.ts b/x-pack/plugins/license_management/public/shared_imports.ts new file mode 100644 index 0000000000000..695432684a660 --- /dev/null +++ b/x-pack/plugins/license_management/public/shared_imports.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 { SectionLoading } from '../../../../src/plugins/es_ui_shared/public/'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0a6019dbd5111..a57851dcb700f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12977,11 +12977,7 @@ "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseButtonLabel": "ライセンスを更新", "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseTitle": "ライセンスの更新", "xpack.licenseMgmt.licenseDashboard.addLicense.useAvailableLicenseDescription": "すでに新しいライセンスがある場合は、今すぐアップロードしてください。", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusDescription": "ライセンスは{expiryDate}に期限切れになります", "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText": "アクティブ", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは{status}です", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusDescription": "ご使用のライセンスは{expiryDate}に期限切れになりました", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは期限切れです", "xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText": "非アクティブ", "xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "ご使用のライセンスには有効期限がありません。", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "トライアルを延長", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index afd6b2208c711..98016266b4ede 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13151,11 +13151,7 @@ "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseButtonLabel": "更新许可证", "xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseTitle": "更新您的许可证", "xpack.licenseMgmt.licenseDashboard.addLicense.useAvailableLicenseDescription": "如果已有新的许可证,请立即上传。", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusDescription": "您的许可证将于 {expiryDate}过期", "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText": "活动", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusTitle": "您的{typeTitleCase}许可证{status}", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusDescription": "您的许可证已于 {expiryDate}过期", - "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusTitle": "您的{typeTitleCase}许可证已过期", "xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText": "非活动", "xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "您的许可证永不会过期。", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "延期试用", From a4440913229a89705356764267b9337edf60d975 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 05:50:25 -0400 Subject: [PATCH 037/118] [Security solution][Endpoint] Removes 'none' compression as it not used anymore (#102767) (#102857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Removes 'none' compression as it not used anymore * Revert type because none type is needed for the first time the artifact is created befor the compression Co-authored-by: David Sánchez --- .../services/artifacts/manifest_manager/manifest_manager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 27108a03f3403..f2d1d3660d78e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -380,7 +380,6 @@ export class ManifestManager { for (const result of results) { await iterateArtifactsBuildResult(result, async (artifact, policyId) => { const artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact; - artifactToAdd.compressionAlgorithm = 'none'; if (!internalArtifactCompleteSchema.is(artifactToAdd)) { throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`); } From c73521505181ccd70743db502f8f5a9a60f6b994 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 06:01:54 -0400 Subject: [PATCH 038/118] unksip functional test (#102633) (#102860) Co-authored-by: Joe Reuter --- test/functional/page_objects/time_to_visualize_page.ts | 5 ++++- x-pack/test/functional/apps/lens/add_to_dashboard.ts | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 287b03ec60d88..57a22103f6409 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -51,7 +51,10 @@ export class TimeToVisualizePageObject extends FtrService { vizName: string, { saveAsNew, redirectToOrigin, addToDashboard, dashboardId, saveToLibrary }: SaveModalArgs = {} ) { - await this.testSubjects.setValue('savedObjectTitle', vizName); + await this.testSubjects.setValue('savedObjectTitle', vizName, { + typeCharByChar: true, + clearWithKeyboard: true, + }); const hasSaveAsNew = await this.testSubjects.exists('saveAsNewCheckbox'); if (hasSaveAsNew && saveAsNew !== undefined) { diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/add_to_dashboard.ts index 61b0c63d226fa..5e51573e32503 100644 --- a/x-pack/test/functional/apps/lens/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/add_to_dashboard.ts @@ -62,8 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); }; - // flaky https://github.com/elastic/kibana/issues/102332 - describe.skip('lens add-to-dashboards tests', () => { + describe('lens add-to-dashboards tests', () => { it('should allow new lens to be added by value to a new dashboard', async () => { await createNewLens(); await PageObjects.lens.save('New Lens from Modal', false, false, false, 'new'); From bf0ccb10339dedb1a599491d8bb865e63b4796f2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 06:13:18 -0400 Subject: [PATCH 039/118] [Metrics] Update ActionsMenu create alert styles (#102316) (#102862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Metrics] Add divider in the actions menu * [Metrics] Add color and icon to the alert link Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Casper Hübertz --- .../inventory_view/components/waffle/node_context_menu.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 94b16448a6b61..ea80bd13e8a4d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -25,6 +25,7 @@ import { SectionSubtitle, SectionLinks, SectionLink, + ActionMenuDivider, } from '../../../../../../../observability/public'; import { useLinkProps } from '../../../../../hooks/use_link_props'; @@ -173,7 +174,10 @@ export const NodeContextMenu: React.FC = withTheme - + + + +
From 6e07800bf29ee184b8e36f2846037efd46207679 Mon Sep 17 00:00:00 2001 From: Katrin Freihofner Date: Tue, 22 Jun 2021 12:26:27 +0200 Subject: [PATCH 040/118] Wording update for case settings, fixes #102462 (#102496) (#102867) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/configure_cases/translations.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 1a60521667bba..ca41db577700e 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -12,7 +12,7 @@ export * from '../../common/translations'; export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( 'xpack.cases.configureCases.incidentManagementSystemTitle', { - defaultMessage: 'Connect to external incident management system', + defaultMessage: 'External incident management system', } ); @@ -20,7 +20,7 @@ export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( 'xpack.cases.configureCases.incidentManagementSystemDesc', { defaultMessage: - 'You may optionally connect cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + 'Connect your cases to an external incident management system. You can then push case data as an incident in a third-party system.', } ); @@ -38,7 +38,7 @@ export const ADD_NEW_CONNECTOR = i18n.translate('xpack.cases.configureCases.addN export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsTitle', { - defaultMessage: 'Case Closures', + defaultMessage: 'Case closures', } ); @@ -46,14 +46,14 @@ export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsDesc', { defaultMessage: - 'Define how you wish cases to be closed. Automated case closures require an established connection to an external incident management system.', + 'Define how to close your cases. Automatic closures require an established connection to an external incident management system.', } ); export const CASE_CLOSURE_OPTIONS_SUB_CASES = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsSubCases', { - defaultMessage: 'Automated closures of sub-cases is not currently supported.', + defaultMessage: 'Automatic closure of sub-cases is not supported.', } ); From fe2ef9361143910072b509af7e473a619579d089 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 22 Jun 2021 13:27:47 +0300 Subject: [PATCH 041/118] [Cases] Add owner and description properties to `kibana.json` (#102707) (#102869) --- x-pack/plugins/cases/kibana.json | 43 +++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 4a85a64c7e03a..6439f28b958d0 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -1,18 +1,31 @@ { - "configPath": ["xpack", "cases"], - "id": "cases", - "kibanaVersion": "kibana", - "extraPublicDirs": ["common"], - "requiredPlugins": [ - "actions", - "esUiShared", - "features", - "kibanaReact", - "kibanaUtils", - "triggersActionsUi" + "configPath":[ + "cases", + "xpack" ], - "optionalPlugins": ["spaces", "security"], - "server": true, - "ui": true, - "version": "8.0.0" + "description":"The Case management system in Kibana", + "extraPublicDirs":[ + "common" + ], + "id":"cases", + "kibanaVersion":"kibana", + "optionalPlugins":[ + "security", + "spaces" + ], + "owner":{ + "githubTeam":"security-threat-hunting", + "name":"Security Solution Threat Hunting" + }, + "requiredPlugins":[ + "actions", + "esUiShared", + "features", + "kibanaReact", + "kibanaUtils", + "triggersActionsUi" + ], + "server":true, + "ui":true, + "version":"8.0.0" } From 3c67c35051615a31027259aee0f88fb0631f4b77 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 22 Jun 2021 14:54:37 +0200 Subject: [PATCH 042/118] Add cache-control for assets served via `registerStaticDir` (#102756) (#102889) * Add cache-control for assets served via `registerStaticDir` * fix case * add test for 'dynamic' file content --- src/core/server/http/http_server.test.ts | 127 +++++++++++++++++- src/core/server/http/http_server.ts | 8 +- .../static/compression_available.json | 3 + .../static/compression_available.json.gz | Bin 0 -> 70 bytes .../fixtures/static/some_json.json | 3 + 5 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 src/core/server/http/integration_tests/fixtures/static/compression_available.json create mode 100644 src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz create mode 100644 src/core/server/http/integration_tests/fixtures/static/some_json.json diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 8e01127d1c3b2..a20a225729124 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -7,9 +7,10 @@ */ import { Server } from 'http'; -import { readFileSync } from 'fs'; +import { rmdir, mkdtemp, readFile, writeFile } from 'fs/promises'; import supertest from 'supertest'; import { omit } from 'lodash'; +import { join } from 'path'; import { ByteSizeValue, schema } from '@kbn/config-schema'; import { HttpConfig } from './http_config'; @@ -47,9 +48,9 @@ const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); let certificate: string; let key: string; -beforeAll(() => { - certificate = readFileSync(KBN_CERT_PATH, 'utf8'); - key = readFileSync(KBN_KEY_PATH, 'utf8'); +beforeAll(async () => { + certificate = await readFile(KBN_CERT_PATH, 'utf8'); + key = await readFile(KBN_KEY_PATH, 'utf8'); }); beforeEach(() => { @@ -1409,6 +1410,19 @@ describe('setup contract', () => { }); describe('#registerStaticDir', () => { + const assetFolder = join(__dirname, 'integration_tests', 'fixtures', 'static'); + let tempDir: string; + + beforeAll(async () => { + tempDir = await mkdtemp('cache-test'); + }); + + afterAll(async () => { + if (tempDir) { + await rmdir(tempDir, { recursive: true }); + } + }); + test('does not throw if called after stop', async () => { const { registerStaticDir } = await server.setup(config); await server.stop(); @@ -1416,6 +1430,111 @@ describe('setup contract', () => { registerStaticDir('/path1/{path*}', '/path/to/resource'); }).not.toThrow(); }); + + test('returns correct headers for static assets', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + }); + + test('returns compressed version if present', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/compression_available.json') + .set('accept-encoding', 'gzip') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + expect(response.get('content-encoding')).toEqual('gzip'); + }); + + test('returns uncompressed version if compressed asset is not available', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('accept-encoding', 'gzip') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + expect(response.get('content-encoding')).toBeUndefined(); + }); + + test('returns a 304 if etag value matches', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .expect(200); + + const etag = response.get('etag'); + expect(etag).not.toBeUndefined(); + + await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('If-None-Match', etag) + .expect(304); + }); + + test('serves content if etag values does not match', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + + await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('If-None-Match', `"definitely not a valid etag"`) + .expect(200); + }); + + test('dynamically updates depending on the content of the file', async () => { + const tempFile = join(tempDir, 'some_file.json'); + + const { registerStaticDir, server: innerServer } = await server.setup(config); + registerStaticDir('/static/{path*}', tempDir); + + await server.start(); + + await supertest(innerServer.listener).get('/static/some_file.json').expect(404); + + await writeFile(tempFile, `{ "over": 9000 }`); + + let response = await supertest(innerServer.listener) + .get('/static/some_file.json') + .expect(200); + + const etag1 = response.get('etag'); + + await writeFile(tempFile, `{ "over": 42 }`); + + response = await supertest(innerServer.listener).get('/static/some_file.json').expect(200); + + const etag2 = response.get('etag'); + + expect(etag1).not.toEqual(etag2); + }); }); describe('#registerOnPreRouting', () => { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 8b4c3b9416152..d43d86d587d06 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -465,7 +465,13 @@ export class HttpServer { lookupCompressed: true, }, }, - options: { auth: false }, + options: { + auth: false, + cache: { + privacy: 'public', + otherwise: 'must-revalidate', + }, + }, }); } diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json b/src/core/server/http/integration_tests/fixtures/static/compression_available.json new file mode 100644 index 0000000000000..1f878fb465cff --- /dev/null +++ b/src/core/server/http/integration_tests/fixtures/static/compression_available.json @@ -0,0 +1,3 @@ +{ + "hello": "dolly" +} diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..e77819d2e8e59a357c56c3c74624d3b82476bdf1 GIT binary patch literal 70 zcmV-M0J;AkiwFp-o6%qZ17mM(aB^jHb7^mGUtxA(X>4I)Y-KKLb8l_{tL9QrP|8Tn c$;nr;Qcz0C&&jD&;;Q8W0MZJ;EEfO(0RJ`}XaE2J literal 0 HcmV?d00001 diff --git a/src/core/server/http/integration_tests/fixtures/static/some_json.json b/src/core/server/http/integration_tests/fixtures/static/some_json.json new file mode 100644 index 0000000000000..c8c4105eb57cd --- /dev/null +++ b/src/core/server/http/integration_tests/fixtures/static/some_json.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} From de22956d00bf719e5e9bcf598d3cb86307cb5dde Mon Sep 17 00:00:00 2001 From: Katrin Freihofner Date: Tue, 22 Jun 2021 15:24:48 +0200 Subject: [PATCH 043/118] Renamed button and dropdown items in headers (apm, logs, metrics and uptime) from alerts to rules (#100918) (#102893) Co-authored-by: Jonathan Buttner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christos Nasikas Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Co-authored-by: Steph Milovic Co-authored-by: Jonathan Buttner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christos Nasikas Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Co-authored-by: Steph Milovic --- .../alerting_popover_flyout.tsx | 8 ++-- .../components/metrics_alert_dropdown.tsx | 12 +++--- .../manage_alerts_context_menu_item.tsx | 2 +- .../components/alert_dropdown.tsx | 4 +- .../public/alerts/configuration.tsx | 10 ++--- .../public/pages/overview/empty_section.ts | 2 +- .../translations/translations/ja-JP.json | 38 ------------------- .../translations/translations/zh-CN.json | 38 ------------------- .../header/action_menu_content.test.tsx | 6 +-- .../alerts/toggle_alert_flyout_button.tsx | 6 +-- .../overview/alerts/translations.ts | 19 ++++++---- .../__snapshots__/monitor_list.test.tsx.snap | 4 +- .../columns/define_connectors.tsx | 14 +++---- .../columns/enable_alert.test.tsx | 4 +- .../monitor_list/columns/translations.ts | 4 +- .../monitor_list_drawer/enabled_alerts.tsx | 6 +-- .../public/lib/alert_types/alert_messages.tsx | 2 +- .../uptime/public/state/alerts/alerts.ts | 6 +-- 18 files changed, 55 insertions(+), 130 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx index 5b4f4e24af44d..ca73f6ddd05b3 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx @@ -18,7 +18,7 @@ import { AlertType } from '../../../../common/alert_types'; import { AlertingFlyout } from '../../alerting/alerting_flyout'; const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', { - defaultMessage: 'Alerts', + defaultMessage: 'Alerts and rules', }); const transactionDurationLabel = i18n.translate( 'xpack.apm.home.alertsMenu.transactionDuration', @@ -33,11 +33,11 @@ const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', { }); const createThresholdAlertLabel = i18n.translate( 'xpack.apm.home.alertsMenu.createThresholdAlert', - { defaultMessage: 'Create threshold alert' } + { defaultMessage: 'Create threshold rule' } ); const createAnomalyAlertAlertLabel = i18n.translate( 'xpack.apm.home.alertsMenu.createAnomalyAlert', - { defaultMessage: 'Create anomaly alert' } + { defaultMessage: 'Create anomaly rule' } ); const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = @@ -102,7 +102,7 @@ export function AlertingPopoverAndFlyout({ { name: i18n.translate( 'xpack.apm.home.alertsMenu.viewActiveAlerts', - { defaultMessage: 'View active alerts' } + { defaultMessage: 'Manage rules' } ), href: basePath.prepend( '/app/management/insightsAndAlerting/triggersActions/alerts' diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx index 41867053c3a0f..c3327dc3fe85d 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx @@ -36,12 +36,12 @@ export const MetricsAlertDropdown = () => { () => ({ id: 1, title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', { - defaultMessage: 'Infrastructure alerts', + defaultMessage: 'Infrastructure rules', }), items: [ { name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', { - defaultMessage: 'Create inventory alert', + defaultMessage: 'Create inventory rule', }), onClick: () => setVisibleFlyoutType('inventory'), }, @@ -54,12 +54,12 @@ export const MetricsAlertDropdown = () => { () => ({ id: 2, title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', { - defaultMessage: 'Metrics alerts', + defaultMessage: 'Metrics rules', }), items: [ { name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', { - defaultMessage: 'Create threshold alert', + defaultMessage: 'Create threshold rule', }), onClick: () => setVisibleFlyoutType('threshold'), }, @@ -76,7 +76,7 @@ export const MetricsAlertDropdown = () => { const manageAlertsMenuItem = useMemo( () => ({ name: i18n.translate('xpack.infra.alerting.manageAlerts', { - defaultMessage: 'Manage alerts', + defaultMessage: 'Manage rules', }), icon: 'tableOfContents', onClick: manageAlertsLinkProps.onClick, @@ -112,7 +112,7 @@ export const MetricsAlertDropdown = () => { { id: 0, title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', { - defaultMessage: 'Alerts', + defaultMessage: 'Alerts and rules', }), items: firstPanelMenuItems, }, diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx index a6b69a37f780e..c9b6275264f91 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx @@ -17,7 +17,7 @@ export const ManageAlertsContextMenuItem = () => { }); return ( - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx index 66c77fbf875a4..c1733d4af0589 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx @@ -66,13 +66,13 @@ export const AlertDropdown = () => { > , , ]; diff --git a/x-pack/plugins/monitoring/public/alerts/configuration.tsx b/x-pack/plugins/monitoring/public/alerts/configuration.tsx index 5416095671d71..7825fe8e20617 100644 --- a/x-pack/plugins/monitoring/public/alerts/configuration.tsx +++ b/x-pack/plugins/monitoring/public/alerts/configuration.tsx @@ -32,7 +32,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', { - defaultMessage: `Unable to disable alert`, + defaultMessage: `Unable to disable rule`, }), text: err.message, }); @@ -46,7 +46,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', { - defaultMessage: `Unable to enable alert`, + defaultMessage: `Unable to enable rule`, }), text: err.message, }); @@ -60,7 +60,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', { - defaultMessage: `Unable to mute alert`, + defaultMessage: `Unable to mute rule`, }), text: err.message, }); @@ -74,7 +74,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { } catch (err) { Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', { - defaultMessage: `Unable to unmute alert`, + defaultMessage: `Unable to unmute rule`, }), text: err.message, }); @@ -112,7 +112,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { }} > {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { - defaultMessage: `Edit alert`, + defaultMessage: `Edit rule`, })} diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts index 40b1157b29e35..2747b2ecdebc9 100644 --- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -97,7 +97,7 @@ export const getEmptySections = ({ core }: { core: CoreStart }): ISection[] => { 'Are 503 errors stacking up? Are services responding? Is CPU and RAM utilization jumping? See warnings as they happen—not as part of the post-mortem.', }), linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', { - defaultMessage: 'Create alert', + defaultMessage: 'Create rule', }), href: core.http.basePath.prepend( '/app/management/insightsAndAlerting/triggersActions/alerts' diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a57851dcb700f..ce976766e7341 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5496,13 +5496,9 @@ "xpack.apm.header.badge.readOnly.text": "読み取り専用", "xpack.apm.header.badge.readOnly.tooltip": "を保存できませんでした", "xpack.apm.helpMenu.upgradeAssistantLink": "アップグレードアシスタント", - "xpack.apm.home.alertsMenu.alerts": "アラート", "xpack.apm.home.alertsMenu.createAnomalyAlert": "異常アラートを作成", - "xpack.apm.home.alertsMenu.createThresholdAlert": "しきい値アラートを作成", "xpack.apm.home.alertsMenu.errorCount": "エラー数", - "xpack.apm.home.alertsMenu.transactionDuration": "レイテンシ", "xpack.apm.home.alertsMenu.transactionErrorRate": "トランザクションエラー率", - "xpack.apm.home.alertsMenu.viewActiveAlerts": "アクティブアラートを表示", "xpack.apm.home.serviceMapTabLabel": "サービスマップ", "xpack.apm.instancesLatencyDistributionChartLegend": "インスタンス", "xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "前の期間", @@ -10884,20 +10880,10 @@ "xpack.indexLifecycleMgmt.timeline.title": "ポリシー概要", "xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "ウォームフェーズ", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", - "xpack.infra.alerting.alertDropdownTitle": "アラート", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし (グループなし) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", - "xpack.infra.alerting.alertsButton": "アラート", - "xpack.infra.alerting.createInventoryAlertButton": "インベントリアラートの作成", - "xpack.infra.alerting.createThresholdAlertButton": "しきい値アラートを作成", "xpack.infra.alerting.infrastructureDropdownMenu": "インフラストラクチャー", - "xpack.infra.alerting.infrastructureDropdownTitle": "インフラストラクチャーアラート", - "xpack.infra.alerting.logs.alertsButton": "アラート", - "xpack.infra.alerting.logs.createAlertButton": "アラートの作成", - "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", - "xpack.infra.alerting.manageAlerts": "アラートを管理", "xpack.infra.alerting.metricsDropdownMenu": "メトリック", - "xpack.infra.alerting.metricsDropdownTitle": "メトリックアラート", "xpack.infra.alerts.charts.errorMessage": "問題が発生しました", "xpack.infra.alerts.charts.loadingMessage": "読み込み中", "xpack.infra.alerts.charts.noDataMessage": "グラフデータがありません", @@ -15913,13 +15899,8 @@ "xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearchノード「{removed}」がこのクラスターから削除されました。", "xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "このクラスターのElasticsearchノードは変更されていません。", "xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "このクラスターでElasticsearchノード「{restarted}」が再起動しました。", - "xpack.monitoring.alerts.panel.disableAlert.errorTitle": "アラートを無効にできません", "xpack.monitoring.alerts.panel.disableTitle": "無効にする", - "xpack.monitoring.alerts.panel.editAlert": "アラートを編集", - "xpack.monitoring.alerts.panel.enableAlert.errorTitle": "アラートを有効にできません", - "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "アラートをミュートできません", "xpack.monitoring.alerts.panel.muteTitle": "ミュート", - "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "アラートをミュート解除できません", "xpack.monitoring.alerts.rejection.paramDetails.duration.label": "最後の", "xpack.monitoring.alerts.rejection.paramDetails.threshold.label": "{type} 拒否カウントが超過するときに通知", "xpack.monitoring.alerts.searchThreadPoolRejections.description": "検索スレッドプールの拒否数がしきい値を超過するときにアラートを発行します。", @@ -17246,7 +17227,6 @@ "xpack.observability.alertsTable.viewInAppButtonLabel": "アプリで表示", "xpack.observability.alertsTitle": "アラート", "xpack.observability.emptySection.apps.alert.description": "503 エラーが累積していますか?サービスは応答していますか?CPUとRAMの使用量が跳ね上がっていますか?このような警告を、事後にではなく、発生と同時に把握しましょう。", - "xpack.observability.emptySection.apps.alert.link": "アラートの作成", "xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。", "xpack.observability.emptySection.apps.apm.description": "分散アーキテクチャ全体でトランザクションを追跡し、サービスの通信をマップ化して、簡単にパフォーマンスボトルネックを特定できます。", "xpack.observability.emptySection.apps.apm.link": "エージェントのインストール", @@ -23541,8 +23521,6 @@ "xpack.uptime.alerts.tls.validAfterExpiringString": "{relativeDate}日以内、{date}に期限切れになります。", "xpack.uptime.alerts.tls.validBeforeExpiredString": "{relativeDate}日前、{date}以降有効です。", "xpack.uptime.alerts.tls.validBeforeExpiringString": "今から{relativeDate}日間、{date}まで無効です。", - "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "アラート", - "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "アラートコンテキストメニューを開く", "xpack.uptime.apmIntegrationAction.description": "このモニターの検索 APM", "xpack.uptime.apmIntegrationAction.text": "APMデータを表示", "xpack.uptime.availabilityLabelText": "{value} %", @@ -23761,15 +23739,11 @@ "xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping", "xpack.uptime.monitorList.anomalyColumn.label": "レスポンス異常スコア", "xpack.uptime.monitorList.defineConnector.description": "アラートを有効にするには、デフォルトのアラートアクションコネクターを定義してください。", - "xpack.uptime.monitorList.disableDownAlert": "ステータスアラートを無効にする", "xpack.uptime.monitorList.downLineSeries.downLabel": "ダウン", "xpack.uptime.monitorList.drawer.missingLocation": "一部の Heartbeat インスタンスには位置情報が定義されていません。Heartbeat 構成への{link}。", "xpack.uptime.monitorList.drawer.mostRecentRun": "直近のテスト実行", "xpack.uptime.monitorList.drawer.statusRowLocationList": "前回の確認時に\"{status}\"ステータスだった場所のリスト。", "xpack.uptime.monitorList.drawer.url": "Url", - "xpack.uptime.monitorList.enabledAlerts.noAlert": "このモニターではアラートが有効ではありません。", - "xpack.uptime.monitorList.enabledAlerts.title": "有効なアラート", - "xpack.uptime.monitorList.enableDownAlert": "ステータスアラートを有効にする", "xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "ID {id}のモニターの行を展開", "xpack.uptime.monitorList.geoName.helpLinkAnnotation": "場所を追加", "xpack.uptime.monitorList.infraIntegrationAction.container.message": "コンテナーメトリックを表示", @@ -23843,15 +23817,7 @@ "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "最終確認からの経過時間", "xpack.uptime.monitorStatusBar.type.ariaLabel": "モニタータイプ", "xpack.uptime.monitorStatusBar.type.label": "型", - "xpack.uptime.navigateToAlertingButton.content": "アラートを管理", - "xpack.uptime.navigateToAlertingUi": "Uptime を離れてアラート管理ページに移動します", "xpack.uptime.notFountPage.homeLinkText": "ホームへ戻る", - "xpack.uptime.openAlertContextPanel.ariaLabel": "アラートコンテキストパネルを開くと、アラートタイプを選択できます", - "xpack.uptime.openAlertContextPanel.label": "アラートの作成", - "xpack.uptime.overview.alerts.disabled.failed": "アラートを無効にできません。", - "xpack.uptime.overview.alerts.disabled.success": "アラートが正常に無効にされました。", - "xpack.uptime.overview.alerts.enabled.failed": "アラートを有効にできません。", - "xpack.uptime.overview.alerts.enabled.success": "アラートが正常に有効にされました。 ", "xpack.uptime.overview.alerts.enabled.success.description": "この監視が停止しているときには、メッセージが {actionConnectors} に送信されます。", "xpack.uptime.overview.filterButton.label": "{title}フィルターのフィルターグループを展開", "xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "お知らせを読む", @@ -24019,10 +23985,6 @@ "xpack.uptime.synthetics.waterfallChart.labels.timings.ssl": "TLS", "xpack.uptime.synthetics.waterfallChart.labels.timings.wait": "待機中 (TTFB) ", "xpack.uptime.title": "アップタイム", - "xpack.uptime.toggleAlertButton.content": "ステータスアラートを監視", - "xpack.uptime.toggleAlertFlyout.ariaLabel": "アラートの追加ポップアップを開く", - "xpack.uptime.toggleTlsAlertButton.ariaLabel": "TLSアラートの追加ポップアップを開く", - "xpack.uptime.toggleTlsAlertButton.content": "TLSアラート", "xpack.uptime.uptimeFeatureCatalogueTitle": "アップタイム", "xpack.urlDrilldown.click.event.key.documentation": "クリックしたデータポイントの後ろのフィールド名。", "xpack.urlDrilldown.click.event.key.title": "クリックしたフィールドの名前。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 98016266b4ede..6796ea0e40c21 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5526,13 +5526,9 @@ "xpack.apm.header.badge.readOnly.text": "只读", "xpack.apm.header.badge.readOnly.tooltip": "无法保存", "xpack.apm.helpMenu.upgradeAssistantLink": "升级助手", - "xpack.apm.home.alertsMenu.alerts": "告警", "xpack.apm.home.alertsMenu.createAnomalyAlert": "创建异常告警", - "xpack.apm.home.alertsMenu.createThresholdAlert": "创建阈值告警", "xpack.apm.home.alertsMenu.errorCount": "错误计数", - "xpack.apm.home.alertsMenu.transactionDuration": "延迟", "xpack.apm.home.alertsMenu.transactionErrorRate": "事务错误率", - "xpack.apm.home.alertsMenu.viewActiveAlerts": "查看活动告警", "xpack.apm.home.serviceMapTabLabel": "服务地图", "xpack.apm.instancesLatencyDistributionChartLegend": "实例", "xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "上一时段", @@ -11023,20 +11019,10 @@ "xpack.indexLifecycleMgmt.timeline.title": "策略摘要", "xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "温阶段", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", - "xpack.infra.alerting.alertDropdownTitle": "告警", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容 (未分组) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", - "xpack.infra.alerting.alertsButton": "告警", - "xpack.infra.alerting.createInventoryAlertButton": "创建库存告警", - "xpack.infra.alerting.createThresholdAlertButton": "创建阈值告警", "xpack.infra.alerting.infrastructureDropdownMenu": "基础设施", - "xpack.infra.alerting.infrastructureDropdownTitle": "基础架构告警", - "xpack.infra.alerting.logs.alertsButton": "告警", - "xpack.infra.alerting.logs.createAlertButton": "创建告警", - "xpack.infra.alerting.logs.manageAlerts": "管理告警", - "xpack.infra.alerting.manageAlerts": "管理告警", "xpack.infra.alerting.metricsDropdownMenu": "指标", - "xpack.infra.alerting.metricsDropdownTitle": "指标告警", "xpack.infra.alerts.charts.errorMessage": "哇哦,出问题了", "xpack.infra.alerts.charts.loadingMessage": "正在加载", "xpack.infra.alerts.charts.noDataMessage": "没有可用图表数据", @@ -16149,13 +16135,8 @@ "xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearch 节点“{removed}”已从此集群中移除。", "xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "此集群的 Elasticsearch 节点中没有更改。", "xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "此集群中 Elasticsearch 节点“{restarted}”已重新启动。", - "xpack.monitoring.alerts.panel.disableAlert.errorTitle": "无法禁用告警", "xpack.monitoring.alerts.panel.disableTitle": "禁用", - "xpack.monitoring.alerts.panel.editAlert": "编辑告警", - "xpack.monitoring.alerts.panel.enableAlert.errorTitle": "无法启用告警", - "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "无法静音告警", "xpack.monitoring.alerts.panel.muteTitle": "静音", - "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "无法取消告警静音", "xpack.monitoring.alerts.rejection.paramDetails.duration.label": "过去", "xpack.monitoring.alerts.rejection.paramDetails.threshold.label": "当 {type} 拒绝计数超过以下阈值时通知:", "xpack.monitoring.alerts.searchThreadPoolRejections.description": "当搜索线程池中的拒绝数目超过阈值时告警。", @@ -17482,7 +17463,6 @@ "xpack.observability.alertsTable.viewInAppButtonLabel": "在应用中查看", "xpack.observability.alertsTitle": "告警", "xpack.observability.emptySection.apps.alert.description": "503 错误是否越来越多?服务是否响应?CPU 和 RAM 利用率是否激增?实时查看警告,而不是事后再进行剖析。", - "xpack.observability.emptySection.apps.alert.link": "创建告警", "xpack.observability.emptySection.apps.alert.title": "未找到告警。", "xpack.observability.emptySection.apps.apm.description": "通过分布式体系结构跟踪事务并映射服务的交互以轻松发现性能瓶颈。", "xpack.observability.emptySection.apps.apm.link": "安装代理", @@ -23907,8 +23887,6 @@ "xpack.uptime.alerts.tls.validAfterExpiringString": "将在{relativeDate} 天后,即 {date}到期。", "xpack.uptime.alerts.tls.validBeforeExpiredString": "自 {relativeDate} 天前,即 {date}开始生效。", "xpack.uptime.alerts.tls.validBeforeExpiringString": "从现在到 {date}的 {relativeDate} 天里无效。", - "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "告警", - "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "打开告警上下文菜单", "xpack.uptime.apmIntegrationAction.description": "在 APM 中搜索此监测", "xpack.uptime.apmIntegrationAction.text": "显示 APM 数据", "xpack.uptime.availabilityLabelText": "{value} %", @@ -24127,15 +24105,11 @@ "xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping", "xpack.uptime.monitorList.anomalyColumn.label": "响应异常分数", "xpack.uptime.monitorList.defineConnector.description": "要开始启用告警,请在以下位置定义默认告警操作连接器", - "xpack.uptime.monitorList.disableDownAlert": "禁用状态告警", "xpack.uptime.monitorList.downLineSeries.downLabel": "关闭检查", "xpack.uptime.monitorList.drawer.missingLocation": "某些 Heartbeat 实例未定义位置。{link}到您的 Heartbeat 配置。", "xpack.uptime.monitorList.drawer.mostRecentRun": "最新测试运行", "xpack.uptime.monitorList.drawer.statusRowLocationList": "上次检查时状态为“{status}”的位置列表。", "xpack.uptime.monitorList.drawer.url": "URL", - "xpack.uptime.monitorList.enabledAlerts.noAlert": "没有为此监测启用告警。", - "xpack.uptime.monitorList.enabledAlerts.title": "已启用的告警", - "xpack.uptime.monitorList.enableDownAlert": "启用状态告警", "xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "展开 ID {id} 的监测行", "xpack.uptime.monitorList.geoName.helpLinkAnnotation": "添加位置", "xpack.uptime.monitorList.infraIntegrationAction.container.message": "显示容器指标", @@ -24209,15 +24183,7 @@ "xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "自上次检查以来经过的时间", "xpack.uptime.monitorStatusBar.type.ariaLabel": "监测类型", "xpack.uptime.monitorStatusBar.type.label": "类型", - "xpack.uptime.navigateToAlertingButton.content": "管理告警", - "xpack.uptime.navigateToAlertingUi": "离开 Uptime 并前往“Alerting 管理”页面", "xpack.uptime.notFountPage.homeLinkText": "返回主页", - "xpack.uptime.openAlertContextPanel.ariaLabel": "打开告警上下文面板,以便可以选择告警类型", - "xpack.uptime.openAlertContextPanel.label": "创建告警", - "xpack.uptime.overview.alerts.disabled.failed": "无法禁用告警!", - "xpack.uptime.overview.alerts.disabled.success": "已成功禁用告警!", - "xpack.uptime.overview.alerts.enabled.failed": "无法启用告警!", - "xpack.uptime.overview.alerts.enabled.success": "已成功启用告警 ", "xpack.uptime.overview.alerts.enabled.success.description": "此监测关闭时,将有消息发送到 {actionConnectors}。", "xpack.uptime.overview.filterButton.label": "展开筛选 {title} 的筛选组", "xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "阅读公告", @@ -24385,10 +24351,6 @@ "xpack.uptime.synthetics.waterfallChart.labels.timings.ssl": "TLS", "xpack.uptime.synthetics.waterfallChart.labels.timings.wait": "等待中 (TTFB)", "xpack.uptime.title": "运行时间", - "xpack.uptime.toggleAlertButton.content": "监测状态告警", - "xpack.uptime.toggleAlertFlyout.ariaLabel": "打开添加告警浮出控件", - "xpack.uptime.toggleTlsAlertButton.ariaLabel": "打开 TLS 告警浮出控件", - "xpack.uptime.toggleTlsAlertButton.content": "TLS 告警", "xpack.uptime.uptimeFeatureCatalogueTitle": "运行时间", "xpack.urlDrilldown.click.event.key.documentation": "已点击数据点背后的字段名称。", "xpack.urlDrilldown.click.event.key.title": "已点击字段的名称。", diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx index 89d8f38b1e3b3..0265588c3fdeb 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx @@ -14,12 +14,12 @@ describe('ActionMenuContent', () => { it('renders alerts dropdown', async () => { const { getByLabelText, getByText } = render(); - const alertsDropdown = getByLabelText('Open alert context menu'); + const alertsDropdown = getByLabelText('Open alerts and rules context menu'); fireEvent.click(alertsDropdown); await waitFor(() => { - expect(getByText('Create alert')); - expect(getByText('Manage alerts')); + expect(getByText('Create rule')); + expect(getByText('Manage rules')); }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index a1b745d07924e..278958bd1987b 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -67,7 +67,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ > ), @@ -114,7 +114,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ }, { id: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID, - title: 'create alerts', + title: ToggleFlyoutTranslations.toggleAlertFlyoutButtonLabel, items: selectionItems, }, ]; @@ -134,7 +134,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ > } diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts index 00a00a4664cd8..7cfcdabe5562b 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts @@ -283,30 +283,33 @@ export const TlsTranslations = { export const ToggleFlyoutTranslations = { toggleButtonAriaLabel: i18n.translate('xpack.uptime.alertsPopover.toggleButton.ariaLabel', { - defaultMessage: 'Open alert context menu', + defaultMessage: 'Open alerts and rules context menu', }), openAlertContextPanelAriaLabel: i18n.translate('xpack.uptime.openAlertContextPanel.ariaLabel', { - defaultMessage: 'Open the alert context panel so you can choose an alert type', + defaultMessage: 'Open the rule context panel so you can choose a rule type', }), openAlertContextPanelLabel: i18n.translate('xpack.uptime.openAlertContextPanel.label', { - defaultMessage: 'Create alert', + defaultMessage: 'Create rule', }), toggleTlsAriaLabel: i18n.translate('xpack.uptime.toggleTlsAlertButton.ariaLabel', { - defaultMessage: 'Open TLS alert flyout', + defaultMessage: 'Open TLS rule flyout', }), toggleTlsContent: i18n.translate('xpack.uptime.toggleTlsAlertButton.content', { - defaultMessage: 'TLS alert', + defaultMessage: 'TLS rule', }), toggleMonitorStatusAriaLabel: i18n.translate('xpack.uptime.toggleAlertFlyout.ariaLabel', { - defaultMessage: 'Open add alert flyout', + defaultMessage: 'Open add rule flyout', }), toggleMonitorStatusContent: i18n.translate('xpack.uptime.toggleAlertButton.content', { - defaultMessage: 'Monitor status alert', + defaultMessage: 'Monitor status rule', }), navigateToAlertingUIAriaLabel: i18n.translate('xpack.uptime.navigateToAlertingUi', { defaultMessage: 'Leave Uptime and go to Alerting Management page', }), navigateToAlertingButtonContent: i18n.translate('xpack.uptime.navigateToAlertingButton.content', { - defaultMessage: 'Manage alerts', + defaultMessage: 'Manage rules', + }), + toggleAlertFlyoutButtonLabel: i18n.translate('xpack.uptime.alerts.createRulesPanel.title', { + defaultMessage: 'Create rules', }), }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap index 115dab1095dc1..cfdf7afba4e85 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap @@ -1303,7 +1303,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` >
- {!details.error && showFooter && ( - - )} ); } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx index 0bebec61657b4..7fbbf6fd3ffdc 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx @@ -21,6 +21,7 @@ import { EuiPageSideBar, useResizeObserver, } from '@elastic/eui'; +import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { isEqual, sortBy } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -205,7 +206,7 @@ export function DiscoverSidebar({ return result; }, [fields]); - const multiFields = useMemo(() => { + const calculateMultiFields = () => { if (!useNewFieldsApi || !fields) { return undefined; } @@ -224,7 +225,13 @@ export function DiscoverSidebar({ map.set(parent, value); }); return map; - }, [fields, useNewFieldsApi, selectedFields]); + }; + + const [multiFields, setMultiFields] = useState(() => calculateMultiFields()); + + useShallowCompareEffect(() => { + setMultiFields(calculateMultiFields()); + }, [fields, selectedFields, useNewFieldsApi]); const deleteField = useMemo( () => diff --git a/src/plugins/kibana_react/public/field_button/field_button.scss b/src/plugins/kibana_react/public/field_button/field_button.scss index 43f60e4503576..f71e097ab7138 100644 --- a/src/plugins/kibana_react/public/field_button/field_button.scss +++ b/src/plugins/kibana_react/public/field_button/field_button.scss @@ -38,6 +38,7 @@ padding: $euiSizeS; display: flex; align-items: flex-start; + line-height: normal; } .kbnFieldButton__fieldIcon { From a8997715b9bf6f302ecf35a73ee26e2684995491 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 22 Jun 2021 17:33:56 +0200 Subject: [PATCH 049/118] [Maps] bump ems client to 7.14 (#102770) (#102910) --- package.json | 2 +- src/dev/license_checker/config.ts | 2 ++ yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b666ea025a74f..83c28adfb6314 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@elastic/charts": "30.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@7.14.0-canary.6", - "@elastic/ems-client": "7.13.0", + "@elastic/ems-client": "7.14.0", "@elastic/eui": "33.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index ebf56166a8922..b3b7bf5e8eed7 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -10,6 +10,7 @@ // used as dependencies or dev dependencies export const LICENSE_ALLOWED = [ 'Elastic-License', + 'Elastic License 2.0', 'SSPL-1.0 OR Elastic License 2.0', '0BSD', '(BSD-2-Clause OR MIT OR Apache-2.0)', @@ -72,6 +73,7 @@ export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0']; export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint + '@elastic/ems-client@7.14.0': ['Elastic License 2.0'], // TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released 'xmldom@0.1.27': ['MIT'], diff --git a/yarn.lock b/yarn.lock index 313a3138df725..de4b8fddce53e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1413,10 +1413,10 @@ ms "^2.1.3" secure-json-parse "^2.4.0" -"@elastic/ems-client@7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.13.0.tgz#de291a6eb25523e5844a9e74ae72fd2e81a1f4d9" - integrity sha512-VdK5jZdnC+5BSkMRQsqHqrsZ9HttnPjQmCjRlAGuV8y6g0eKVP9ZiMRQFKFKmuSKpx0kHGsSV/1kBglTmSl/3g== +"@elastic/ems-client@7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.14.0.tgz#7c8095086bd9a637f72d6d810d494a460c68e0fc" + integrity sha512-axXTyBrC1I2TMmcxGC04SgODwb5Cp6svcW64RoTr8X2XrSSuH0gh+X5qMsC9FgGGnmbVNCEYIs3JK4AJ7X4bxA== dependencies: "@types/geojson" "^7946.0.7" "@types/lru-cache" "^5.1.0" From c8c48601a997967fbda7c835e7d7c3b838eac3ea Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 11:41:14 -0400 Subject: [PATCH 050/118] [ML] Functional tests - explicitly delete jobs after setupModule tests (#102882) (#102913) This PR explicitly deletes the jobs created by the `setupModule` tests. Co-authored-by: Robert Oskamp --- x-pack/test/api_integration/apis/ml/modules/setup_module.ts | 3 +++ 1 file changed, 3 insertions(+) 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 6011c38255cdc..c4dd529ac14f5 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 @@ -1048,6 +1048,9 @@ export default ({ getService }: FtrProviderContext) => { for (const dashboard of testData.expected.dashboards) { await ml.testResources.deleteDashboardById(dashboard); } + for (const job of testData.expected.jobs) { + await ml.api.deleteAnomalyDetectionJobES(job.jobId); + } await ml.api.cleanMlIndices(); }); From 3f3bd5f94557a29870d2c8b427a8802a39a8956c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 11:55:35 -0400 Subject: [PATCH 051/118] increase chart switch width (#102520) (#102916) Co-authored-by: Joe Reuter --- .../editor_frame/workspace_panel/chart_switch.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss index 3fafa8b37a42f..a4e22b4ef558c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss @@ -22,5 +22,5 @@ img.lnsChartSwitch__chartIcon { // stylelint-disable-line selector-no-qualifying } .lnsChartSwitch__search { - width: 7 * $euiSizeXXL; + width: 10 * $euiSizeXXL; } From 330b0db03a6bec855ba136f63642d6c8bcbef959 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 12:22:34 -0400 Subject: [PATCH 052/118] [Uptime] [Synthetics Integration] transition to monaco code editor (#102642) (#102920) * update synthetics integration code editor * add basic support for xml and javascript * fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dominique Clarke --- packages/kbn-monaco/src/monaco_imports.ts | 4 +- .../components/fleet_package/code_editor.tsx | 45 ++++++++++ .../contexts/advanced_fields_http_context.tsx | 2 +- .../fleet_package/header_field.test.tsx | 4 +- .../components/fleet_package/header_field.tsx | 2 +- .../fleet_package/request_body_field.test.tsx | 4 +- .../fleet_package/request_body_field.tsx | 83 ++++--------------- .../public/components/fleet_package/types.tsx | 13 ++- .../apps/uptime/synthetics_integration.ts | 4 +- 9 files changed, 84 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index 92ea23347c374..3f689e6ec0c01 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -7,7 +7,6 @@ */ /* eslint-disable @kbn/eslint/module_migration */ - import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import 'monaco-editor/esm/vs/base/common/worker/simpleWorker'; @@ -23,4 +22,7 @@ import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight +import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support +import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support + export { monaco }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx b/x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx new file mode 100644 index 0000000000000..d2fe3f9b30e84 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx @@ -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 React from 'react'; + +import styled from 'styled-components'; + +import { EuiPanel } from '@elastic/eui'; +import { CodeEditor as MonacoCodeEditor } from '../../../../../../src/plugins/kibana_react/public'; + +import { MonacoEditorLangId } from './types'; + +const CodeEditorContainer = styled(EuiPanel)` + padding: 0; +`; + +interface Props { + ariaLabel: string; + id: string; + languageId: MonacoEditorLangId; + onChange: (value: string) => void; + value: string; +} + +export const CodeEditor = ({ ariaLabel, id, languageId, onChange, value }: Props) => { + return ( + +
+ +
+
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx index c257a8f71b77a..b51aa6cbf3a7c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx @@ -36,7 +36,7 @@ export const initialValues = { [ConfigKeys.RESPONSE_STATUS_CHECK]: [], [ConfigKeys.REQUEST_BODY_CHECK]: { value: '', - type: Mode.TEXT, + type: Mode.PLAINTEXT, }, [ConfigKeys.REQUEST_HEADERS_CHECK]: {}, [ConfigKeys.REQUEST_METHOD_CHECK]: HTTPMethod.GET, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx index ee33083b3eae9..6d9e578fe53f5 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx @@ -76,14 +76,14 @@ describe('', () => { }); it('handles content mode', async () => { - const contentMode: Mode = Mode.TEXT; + const contentMode: Mode = Mode.PLAINTEXT; render( ); await waitFor(() => { expect(onChange).toBeCalledWith({ - 'Content-Type': contentTypes[Mode.TEXT], + 'Content-Type': contentTypes[Mode.PLAINTEXT], }); }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx index 9f337d4b00704..eaf9be50e9665 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx @@ -61,7 +61,7 @@ export const HeaderField = ({ contentMode, defaultValue, onChange }: Props) => { export const contentTypes: Record = { [Mode.JSON]: ContentType.JSON, - [Mode.TEXT]: ContentType.TEXT, + [Mode.PLAINTEXT]: ContentType.TEXT, [Mode.XML]: ContentType.XML, [Mode.FORM]: ContentType.FORM, }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx index 849809eae52a4..fa666ac764ac7 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import 'jest-canvas-mock'; + import React, { useState, useCallback } from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; import { render } from '../../lib/helper/rtl_helpers'; @@ -16,7 +18,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ })); describe('', () => { - const defaultMode = Mode.TEXT; + const defaultMode = Mode.PLAINTEXT; const defaultValue = 'sample value'; const WrappedComponent = () => { const [config, setConfig] = useState({ diff --git a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx index 1ef8fdd75e7f3..1fdde7c2b63fc 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx @@ -5,67 +5,13 @@ * 2.0. */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { stringify, parse } from 'query-string'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { stringify, parse } from 'query-string'; - -import styled from 'styled-components'; - -import { EuiCodeEditor, EuiPanel, EuiTabbedContent } from '@elastic/eui'; - -import { Mode } from './types'; - +import { EuiTabbedContent } from '@elastic/eui'; +import { Mode, MonacoEditorLangId } from './types'; import { KeyValuePairsField, Pair } from './key_value_field'; - -import 'brace/theme/github'; -import 'brace/mode/xml'; -import 'brace/mode/json'; -import 'brace/ext/language_tools'; - -const CodeEditorContainer = styled(EuiPanel)` - padding: 0; -`; - -enum ResponseBodyType { - CODE = 'code', - FORM = 'form', -} - -const CodeEditor = ({ - ariaLabel, - id, - mode, - onChange, - value, -}: { - ariaLabel: string; - id: string; - mode: Mode; - onChange: (value: string) => void; - value: string; -}) => { - return ( - -
- -
-
- ); -}; +import { CodeEditor } from './code_editor'; interface Props { onChange: (requestBody: { type: Mode; value: string }) => void; @@ -73,6 +19,11 @@ interface Props { value: string; } +enum ResponseBodyType { + CODE = 'code', + FORM = 'form', +} + // TO DO: Look into whether or not code editor reports errors, in order to prevent form submission on an error export const RequestBodyField = ({ onChange, type, value }: Props) => { const [values, setValues] = useState>({ @@ -129,9 +80,9 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => { const tabs = [ { - id: Mode.TEXT, - name: modeLabels[Mode.TEXT], - 'data-test-subj': `syntheticsRequestBodyTab__${Mode.TEXT}`, + id: Mode.PLAINTEXT, + name: modeLabels[Mode.PLAINTEXT], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.PLAINTEXT}`, content: ( { defaultMessage: 'Text code editor', } )} - id={Mode.TEXT} - mode={Mode.TEXT} + id={Mode.PLAINTEXT} + languageId={MonacoEditorLangId.PLAINTEXT} onChange={(code) => setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code })) } @@ -162,7 +113,7 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => { } )} id={Mode.JSON} - mode={Mode.JSON} + languageId={MonacoEditorLangId.JSON} onChange={(code) => setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code })) } @@ -183,7 +134,7 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => { } )} id={Mode.XML} - mode={Mode.XML} + languageId={MonacoEditorLangId.XML} onChange={(code) => setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code })) } @@ -229,7 +180,7 @@ const modeLabels = { defaultMessage: 'Form', } ), - [Mode.TEXT]: i18n.translate( + [Mode.PLAINTEXT]: i18n.translate( 'xpack.uptime.createPackagePolicy.stepConfigure.requestBodyType.text', { defaultMessage: 'Text', diff --git a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx index 4d44b4f074e82..7a16d1352c40a 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx @@ -25,10 +25,17 @@ export enum ResponseBodyIndexPolicy { ON_ERROR = 'on_error', } +export enum MonacoEditorLangId { + JSON = 'xjson', + PLAINTEXT = 'plaintext', + XML = 'xml', + JAVASCRIPT = 'javascript', +} + export enum Mode { FORM = 'form', JSON = 'json', - TEXT = 'text', + PLAINTEXT = 'text', XML = 'xml', } @@ -192,11 +199,11 @@ export interface PolicyConfig { [DataStream.ICMP]: ICMPFields; } -export type Validation = Partial void>>; +export type Validation = Partial boolean>>; export const contentTypesToMode = { [ContentType.FORM]: Mode.FORM, [ContentType.JSON]: Mode.JSON, - [ContentType.TEXT]: Mode.TEXT, + [ContentType.TEXT]: Mode.PLAINTEXT, [ContentType.XML]: Mode.XML, }; diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts index 0872abfcaa4f8..a4740de8e9a2b 100644 --- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts +++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts @@ -277,7 +277,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, requestBody: { type: 'xml', - value: 'samplexml', + value: 'samplexml', }, indexResponseBody: false, indexResponseHeaders: false, @@ -308,7 +308,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, 'check.response.headers': advancedConfig.responseHeaders, 'check.response.status': [advancedConfig.responseStatusCheck], - 'check.request.body': `${advancedConfig.requestBody.value}`, // code editor adds closing tag + 'check.request.body': advancedConfig.requestBody.value, 'check.response.body.positive': [advancedConfig.responseBodyCheckPositive], 'check.response.body.negative': [advancedConfig.responseBodyCheckNegative], 'response.include_body': advancedConfig.indexResponseBody ? 'on_error' : 'never', From 623c77b50af99de84f92b0d894a311e50f693b4b Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 22 Jun 2021 18:29:38 +0200 Subject: [PATCH 053/118] [7.x] Move ES aggregation types to src/core (#102597) (#102921) --- .../index.d.ts => src/core/types/elasticsearch/index.ts | 2 +- .../search.d.ts => src/core/types/elasticsearch/search.ts | 0 .../components/app/RumDashboard/LocalUIFilters/index.tsx | 2 +- .../components/app/RumDashboard/LocalUIFilters/queries.ts | 2 +- .../apm/public/components/shared/KueryBar/get_bool_filter.ts | 2 +- x-pack/plugins/apm/public/utils/testHelpers.tsx | 2 +- x-pack/plugins/apm/scripts/shared/get_es_client.ts | 2 +- x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts | 2 +- .../register_transaction_duration_anomaly_alert_type.ts | 2 +- .../server/lib/apm_telemetry/collect_data_telemetry/index.ts | 2 +- .../errors/get_correlations_for_failed_transactions.ts | 4 ++-- x-pack/plugins/apm/server/lib/correlations/get_filters.ts | 2 +- .../latency/get_correlations_for_slow_transactions.ts | 2 +- .../lib/correlations/latency/get_duration_for_percentile.ts | 2 +- .../lib/correlations/latency/get_latency_distribution.ts | 4 ++-- .../apm/server/lib/correlations/latency/get_max_latency.ts | 2 +- .../server/lib/correlations/process_significant_term_aggs.ts | 2 +- .../plugins/apm/server/lib/errors/distribution/get_buckets.ts | 2 +- .../add_filter_to_exclude_legacy_data.ts | 2 +- .../helpers/create_es_client/create_apm_event_client/index.ts | 2 +- .../create_apm_event_client/unpack_processor_events.ts | 2 +- .../create_es_client/create_internal_es_client/index.ts | 2 +- .../plugins/apm/server/lib/helpers/transaction_error_rate.ts | 2 +- .../apm/server/lib/metrics/fetch_and_transform_metrics.ts | 2 +- .../plugins/apm/server/lib/metrics/transform_metrics_chart.ts | 2 +- .../apm/server/lib/rum_client/ui_filters/get_es_filter.ts | 2 +- .../apm/server/lib/service_map/get_service_anomalies.ts | 2 +- .../lib/service_map/get_service_map_service_node_info.ts | 2 +- .../apm/server/lib/service_map/get_trace_sample_ids.ts | 2 +- .../services/annotations/get_derived_service_annotations.ts | 2 +- .../server/lib/services/annotations/get_stored_annotations.ts | 2 +- .../plugins/apm/server/lib/services/annotations/index.test.ts | 2 +- .../get_service_instances_system_metric_statistics.ts | 2 +- x-pack/plugins/apm/server/lib/services/get_throughput.ts | 2 +- .../services/profiling/get_service_profiling_statistics.ts | 2 +- .../agent_configuration/convert_settings_to_string.ts | 2 +- .../settings/agent_configuration/find_exact_configuration.ts | 2 +- .../lib/settings/agent_configuration/search_configurations.ts | 2 +- x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts | 2 +- .../apm/server/lib/transactions/get_anomaly_data/fetcher.ts | 2 +- .../apm/server/lib/transactions/get_latency_charts/index.ts | 2 +- .../server/lib/transactions/get_throughput_charts/index.ts | 2 +- x-pack/plugins/apm/server/projections/typings.ts | 2 +- x-pack/plugins/apm/server/utils/queries.ts | 2 +- x-pack/plugins/apm/server/utils/test_helpers.tsx | 2 +- x-pack/plugins/fleet/server/services/agents/helpers.ts | 2 +- .../fleet/server/services/api_keys/enrollment_api_key.ts | 2 +- x-pack/plugins/fleet/server/services/artifacts/mappings.ts | 2 +- x-pack/plugins/fleet/server/services/artifacts/mocks.ts | 2 +- x-pack/plugins/lens/server/routes/field_stats.ts | 2 +- x-pack/plugins/lens/server/usage/task.ts | 2 +- .../series_builder/columns/report_definition_field.tsx | 2 +- .../series_editor/columns/filter_expanded.tsx | 2 +- .../public/components/shared/field_value_suggestions/types.ts | 2 +- x-pack/plugins/observability/public/hooks/use_es_search.ts | 2 +- x-pack/plugins/observability/public/hooks/use_values_list.ts | 2 +- x-pack/plugins/rule_registry/server/rule_data_client/types.ts | 2 +- .../server/utils/create_persistence_rule_type_factory.ts | 2 +- .../server/lib/detection_engine/reference_rules/query.ts | 2 +- .../signals/threshold/get_threshold_bucket_filters.ts | 2 +- .../server/lib/machine_learning/index.test.ts | 2 +- .../plugins/stack_alerts/common/build_sorted_events_query.ts | 2 +- .../server/alert_types/es_query/alert_type.test.ts | 2 +- .../task_manager/server/monitoring/workload_statistics.ts | 2 +- x-pack/plugins/uptime/server/lib/lib.ts | 2 +- .../uptime/server/lib/requests/get_journey_failed_steps.ts | 2 +- .../plugins/uptime/server/lib/requests/get_journey_steps.ts | 2 +- .../plugins/uptime/server/lib/requests/get_snapshot_counts.ts | 2 +- .../uptime/server/lib/requests/search/query_context.ts | 2 +- 69 files changed, 70 insertions(+), 70 deletions(-) rename typings/elasticsearch/index.d.ts => src/core/types/elasticsearch/index.ts (94%) rename typings/elasticsearch/search.d.ts => src/core/types/elasticsearch/search.ts (100%) diff --git a/typings/elasticsearch/index.d.ts b/src/core/types/elasticsearch/index.ts similarity index 94% rename from typings/elasticsearch/index.d.ts rename to src/core/types/elasticsearch/index.ts index 1951434890c50..bec611778e6f5 100644 --- a/typings/elasticsearch/index.d.ts +++ b/src/core/types/elasticsearch/index.ts @@ -29,4 +29,4 @@ export type ESSearchResponse< TOptions extends { restTotalHitsAsInt: boolean } = { restTotalHitsAsInt: false } > = InferSearchResponseOf; -export { InferSearchResponseOf, AggregationResultOf, SearchHit }; +export type { InferSearchResponseOf, AggregationResultOf, SearchHit }; diff --git a/typings/elasticsearch/search.d.ts b/src/core/types/elasticsearch/search.ts similarity index 100% rename from typings/elasticsearch/search.d.ts rename to src/core/types/elasticsearch/search.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index e09cc2a81c927..abcacbe89587b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -15,7 +15,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ESFilter } from 'typings/elasticsearch'; +import { ESFilter } from 'src/core/types/elasticsearch'; import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; import { uxFiltersByName, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts index 5fe6bc725ef28..12f1fc0f0faea 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from 'typings/elasticsearch'; +import { ESFilter } from 'src/core/types/elasticsearch'; import { SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index c86cf769d7529..150a4d9efc2cb 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ERROR_GROUP_ID, PROCESSOR_EVENT, diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 877dec14ca1b0..9a1d4da8ece7c 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -19,7 +19,7 @@ import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common import { ESSearchRequest, ESSearchResponse, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../observability/typings/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../server'; diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts index 7a8e09423ff15..3accb832fb0ed 100644 --- a/x-pack/plugins/apm/scripts/shared/get_es_client.ts +++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts @@ -10,7 +10,7 @@ import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../src/core/types/elasticsearch'; export type ESClient = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts index 7d5b7d594bdf9..8b4d3e2186c84 100644 --- a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -8,7 +8,7 @@ import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../src/core/types/elasticsearch'; import { AlertServices } from '../../../../alerting/server'; export async function alertingEsClient({ diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 4ced6e6abb251..f640925b0a0fa 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { compact } from 'lodash'; -import { ESSearchResponse } from 'typings/elasticsearch'; +import { ESSearchResponse } from 'src/core/types/elasticsearch'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { ALERT_EVALUATION_THRESHOLD, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 87686d2c30cae..22a2090dbb6cd 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -11,7 +11,7 @@ import { IndicesStats } from '@elastic/elasticsearch/api/requestParams'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../src/core/types/elasticsearch'; import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; import { tasks } from './tasks'; import { APMDataTelemetry } from '../types'; diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts index 11e9f99ddb356..081c66dc2c471 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts @@ -11,8 +11,8 @@ import { processSignificantTermAggs, TopSigTerm, } from '../process_significant_term_aggs'; -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts index 92fc9c5d9622b..61fec492ad38e 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts @@ -6,7 +6,7 @@ */ import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { environmentQuery, rangeQuery, kqlQuery } from '../../utils/queries'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts index c37b3e3ab8242..868a36958395b 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { getDurationForPercentile } from './get_duration_for_percentile'; diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts index a686980700d83..902bdb8c7b511 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts index be1bb631378cf..ad11d21a710d0 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../src/core/types/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { TopSigTerm } from '../process_significant_term_aggs'; diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts index f2762086614b4..8b9a6c064b4a0 100644 --- a/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts index cc1e32e47973d..ecb751cad5a3f 100644 --- a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts +++ b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts @@ -9,7 +9,7 @@ import { orderBy } from 'lodash'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../src/core/types/elasticsearch'; export interface TopSigTerm { fieldName: string; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index a51464764f2b4..fa73ce8f2bc85 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ERROR_GROUP_ID, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts index 96bc8897e62fd..60984d65f4499 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -10,7 +10,7 @@ import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fiel import { ESSearchRequest, ESFilter, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; /* Adds a range query to the ES request to exclude legacy data diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 916a6981f286a..0a464982b6e81 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -15,7 +15,7 @@ import { import { ESSearchRequest, InferSearchResponseOf, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; import { unwrapEsResponse } from '../../../../../../observability/server'; import { ProcessorEvent } from '../../../../../common/processor_event'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 76e615f42bb64..8732ba81f9ae6 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -11,7 +11,7 @@ import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESFilter, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; import { APMEventESSearchRequest } from '.'; import { ApmIndicesConfig, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index e6b61a709ae35..eb3deb2889360 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -12,7 +12,7 @@ import { APMRouteHandlerResources } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../src/core/types/elasticsearch'; import { callAsyncWithDebug, getDebugBody, diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index b60a2a071e6dc..41d9c373710c1 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -10,7 +10,7 @@ import { EventOutcome } from '../../../common/event_outcome'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../src/core/types/elasticsearch'; export const getOutcomeAggregation = () => ({ terms: { diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index a91571bbc406d..cd94eb8511282 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -6,7 +6,7 @@ */ import { Overwrite, Unionize } from 'utility-types'; -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; import { getMetricsProjection } from '../../projections/metrics'; import { mergeProjection } from '../../projections/util/merge_projection'; import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 17759f9094a87..999830dabefc4 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -6,7 +6,7 @@ */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { GenericMetricsRequest } from './fetch_and_transform_metrics'; import { ChartBase } from './types'; diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts index ce6de1e007625..48beb9bca5241 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts @@ -9,7 +9,7 @@ import { uxLocalUIFilterNames, uxLocalUIFilters, } from '../../../../common/ux_ui_filter'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { UxUIFilters } from '../../../../typings/ui_filters'; import { environmentQuery } from '../../../utils/queries'; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 129a0ee73b8cb..7ac56bcd9192d 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -8,7 +8,7 @@ import Boom from '@hapi/boom'; import { sortBy, uniqBy } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; -import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { MlPluginSetup } from '../../../../ml/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { getSeverity, ML_ERRORS } from '../../../common/anomaly_detection'; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 2709fb640d8ce..2e0ac303e5157 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 7894a95cf4d7e..26d7d2d1ee316 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -8,7 +8,7 @@ import Boom from '@hapi/boom'; import { sortBy, take, uniq } from 'lodash'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 202b5075d2ea7..08587217980fb 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -6,7 +6,7 @@ */ import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 3e1a8f26de6b4..56b7aa1f465b0 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -12,7 +12,7 @@ import { unwrapEsResponse, WrappedElasticsearchClientError, } from '../../../../../observability/server'; -import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../src/core/types/elasticsearch'; import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { Annotation, AnnotationType } from '../../../../common/annotations'; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index e2597a4a79cba..6d65c971baa33 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -8,7 +8,7 @@ import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../src/core/types/elasticsearch'; import { inspectSearchParams, SearchParamsMock, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts index 526ae19143f13..b817d4fb654ce 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AggregationOptionsByType } from 'typings/elasticsearch'; +import { AggregationOptionsByType } from 'src/core/types/elasticsearch'; import { METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_PROCESS_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index b0cb917d302fc..0490c31e7c63d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { SERVICE_NAME, TRANSACTION_TYPE, diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index bb98abf724db4..4e88c752aa50b 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -15,7 +15,7 @@ import { getValueTypeConfig, } from '../../../../common/profiling'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PROFILE_STACK, PROFILE_TOP_ID, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts index 0b826ea10b6c4..3ec10f6bf0c63 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../src/core/types/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; // needed for backwards compatability diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts index 3543d38f7b5d1..90f82442f9bfa 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../src/core/types/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 4e27953b3a315..1e37ae9108573 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SearchHit } from '../../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../../src/core/types/elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index c1bf363b49d1c..85f36b3999060 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -9,7 +9,7 @@ import { sortBy, take } from 'lodash'; import moment from 'moment'; import { Unionize } from 'utility-types'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index b105f3b5c0a30..558db17939354 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -6,7 +6,7 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { rangeQuery } from '../../../../server/utils/queries'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index 2d350090fa28b..1a183e15fee2b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -6,7 +6,7 @@ */ import { offsetPreviousPeriodCoordinates } from '../../../../common/utils/offset_previous_period_coordinate'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts index f4d9236395252..ed85e700c3473 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts index 60a3317af1864..0843fa4c9dd64 100644 --- a/x-pack/plugins/apm/server/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -5,7 +5,7 @@ * 2.0. */ import { estypes } from '@elastic/elasticsearch'; -import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../src/core/types/elasticsearch'; import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; export type Projection = Omit & { diff --git a/x-pack/plugins/apm/server/utils/queries.ts b/x-pack/plugins/apm/server/utils/queries.ts index f21ef9de0283f..a82b49a84dc6e 100644 --- a/x-pack/plugins/apm/server/utils/queries.ts +++ b/x-pack/plugins/apm/server/utils/queries.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../src/core/types/elasticsearch'; import { SERVICE_ENVIRONMENT } from '../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 9f271c566e2fa..1d0a47ece9a60 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -10,7 +10,7 @@ import { PromiseReturnType } from '../../../observability/typings/common'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../src/core/types/elasticsearch'; import { UxUIFilters } from '../../typings/ui_filters'; interface Options { diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 822a2a9df98d5..2618aad38bfbf 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -7,7 +7,7 @@ import type { estypes } from '@elastic/elasticsearch'; -import type { SearchHit } from '../../../../../../typings/elasticsearch'; +import type { SearchHit } from '../../../../../../src/core/types/elasticsearch'; import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; type FleetServerAgentESResponse = diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index ba34dbba108d7..14d43e6e219db 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -12,7 +12,7 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { esKuery } from '../../../../../../src/plugins/data/server'; -import type { ESSearchResponse as SearchResponse } from '../../../../../../typings/elasticsearch'; +import type { ESSearchResponse as SearchResponse } from '../../../../../../src/core/types/elasticsearch'; import type { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; import { IngestManagerError } from '../../errors'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; diff --git a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts index 79d14a27fa827..10dab0400d5aa 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SearchHit } from '../../../../../../typings/elasticsearch'; +import type { SearchHit } from '../../../../../../src/core/types/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties, NewArtifact } from './types'; import { ARTIFACT_DOWNLOAD_RELATIVE_PATH } from './constants'; diff --git a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts index 1a10f93f678b3..8bc1768da23a2 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts @@ -10,7 +10,7 @@ import type { ApiResponse } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; -import type { SearchHit, ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import type { SearchHit, ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties, ArtifactsClientInterface } from './types'; import { newArtifactToElasticsearchProperties } from './mappings'; diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index ef1043ddd3583..12d3ef3f4a95e 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; import { SavedObjectNotFound } from '../../../../../src/plugins/kibana_utils/common'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; import { PluginStartContract } from '../plugin'; diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 9c9ab7fd0b350..9227ca885359b 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -16,7 +16,7 @@ import { } from '../../../task_manager/server'; import { getVisualizationCounts } from './visualization_counts'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; // This task is responsible for running daily and aggregating all the Lens click event objects // into daily rolled-up documents, which will be used in reporting click stats diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index a4c4b4d1c78c0..d36e33f16424c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -11,7 +11,7 @@ import { isEmpty } from 'lodash'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { ESFilter } from '../../../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; import { buildPhrasesFilter } from '../../configurations/utils'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 17d62b68c57e4..a78f6adeca39f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -18,7 +18,7 @@ import { DataSeries, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { ESFilter } from '../../../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters'; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 48745c5a8f8a6..ab24f4064c02e 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -7,7 +7,7 @@ import { PopoverAnchorPosition } from '@elastic/eui'; import { Dispatch, SetStateAction } from 'react'; -import { ESFilter } from 'typings/elasticsearch'; +import { ESFilter } from 'src/core/types/elasticsearch'; interface CommonProps { selectedValue?: string[]; diff --git a/x-pack/plugins/observability/public/hooks/use_es_search.ts b/x-pack/plugins/observability/public/hooks/use_es_search.ts index 18a846a6f85d2..27c4081a99775 100644 --- a/x-pack/plugins/observability/public/hooks/use_es_search.ts +++ b/x-pack/plugins/observability/public/hooks/use_es_search.ts @@ -7,7 +7,7 @@ import { estypes } from '@elastic/elasticsearch'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { isCompleteResponse } from '../../../../../src/plugins/data/common'; import { useFetcher } from './use_fetcher'; diff --git a/x-pack/plugins/observability/public/hooks/use_values_list.ts b/x-pack/plugins/observability/public/hooks/use_values_list.ts index e563293d26d6f..094b7a0f36921 100644 --- a/x-pack/plugins/observability/public/hooks/use_values_list.ts +++ b/x-pack/plugins/observability/public/hooks/use_values_list.ts @@ -8,7 +8,7 @@ import { capitalize, union } from 'lodash'; import { useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../src/core/types/elasticsearch'; import { createEsParams, useEsSearch } from './use_es_search'; export interface Props { diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 348fca6a58188..d5ce022781b0d 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -9,7 +9,7 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/api/types'; import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor } from 'src/plugins/data/server'; -import { ESSearchRequest, ESSearchResponse } from 'typings/elasticsearch'; +import { ESSearchRequest, ESSearchResponse } from 'src/core/types/elasticsearch'; import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; export interface RuleDataReader { diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts index 0e244fbaa2ee3..3f50b78151e74 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ESSearchRequest } from 'typings/elasticsearch'; +import { ESSearchRequest } from 'src/core/types/elasticsearch'; import v4 from 'uuid/v4'; import { Logger } from '@kbn/logging'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts index 04bfa78f883f0..39f325fd6cf8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; import { Logger } from '@kbn/logging'; -import { ESSearchRequest } from 'typings/elasticsearch'; +import { ESSearchRequest } from 'src/core/types/elasticsearch'; import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts index 4dd21938690db..e6a188a20b5d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -6,7 +6,7 @@ */ import { Filter } from 'src/plugins/data/common'; -import { ESFilter } from '../../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../../src/core/types/elasticsearch'; import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from '../types'; export const getThresholdBucketFilters = async ({ diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts index 30dd5adf6123b..41f6833743797 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { getExceptionListItemSchemaMock } from '../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getAnomalies, AnomaliesSearchParams } from '.'; diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts index 455307cb73a09..83421056229cf 100644 --- a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts @@ -5,7 +5,7 @@ * 2.0. */ import { estypes } from '@elastic/elasticsearch'; -import type { ESSearchRequest } from '../../../../typings/elasticsearch'; +import type { ESSearchRequest } from '../../../../src/core/types/elasticsearch'; interface BuildSortedEventsQueryOpts { aggs?: Record; 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 d195534d93f58..5b450ceba192a 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 @@ -17,7 +17,7 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { getAlertType, ConditionMetAlertInstanceId, ActionGroupId } from './alert_type'; import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; import { ActionContext } from './action_context'; -import { ESSearchResponse, ESSearchRequest } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse, ESSearchRequest } from '../../../../../../src/core/types/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index abd86be522f0c..64c1c66140196 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -13,7 +13,7 @@ import { keyBy, mapValues } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '../lib/intervals'; -import { AggregationResultOf } from '../../../../../typings/elasticsearch'; +import { AggregationResultOf } from '../../../../../src/core/types/elasticsearch'; import { HealthStatus } from './monitoring_stats_stream'; import { TaskStore } from '../task_store'; import { createRunningAveragedStat } from './task_run_calcultors'; diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index e79d3c28a7d3a..cf00841313536 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -17,7 +17,7 @@ import { UMBackendFrameworkAdapter } from './adapters'; import { UMLicenseCheck } from './domains'; import { UptimeRequests } from './requests'; import { savedObjectsAdapter } from './saved_objects'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; export interface UMDomainLibs { requests: UptimeRequests; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts index d14a723d27628..d98e235460167 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts @@ -6,7 +6,7 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { SearchHit } from '../../../../../../typings/elasticsearch'; +import { SearchHit } from '../../../../../../src/core/types/elasticsearch'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index 2e32670aea276..95aadc776fa76 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -6,7 +6,7 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { SearchHit } from 'typings/elasticsearch/search'; +import { SearchHit } from 'src/core/types/elasticsearch/search'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 0e47f2a3d56c2..aef01f29f4d57 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -9,7 +9,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { CONTEXT_DEFAULTS } from '../../../common/constants'; import { Snapshot } from '../../../common/runtime_types'; import { QueryContext } from './search'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; export interface GetSnapshotCountParams { dateRangeStart: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index b54515e84289a..d443411ef4c6e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -10,7 +10,7 @@ import { CursorPagination } from './types'; import { parseRelativeDate } from '../../helper'; import { CursorDirection, SortOrder } from '../../../../common/runtime_types'; import { UptimeESClient } from '../../lib'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; export class QueryContext { callES: UptimeESClient; From a10fb12f25ed248008e44801cffec1745810225b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 12:31:52 -0400 Subject: [PATCH 054/118] [Lens] Update dimension panel copy to suggested one (#102890) (#102925) Co-authored-by: Marco Liberati --- .../editor_frame/config_panel/dimension_container.tsx | 2 +- .../dimension_panel/dimension_editor.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx index 2f3eb5043d610..c62b10093e6e5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx @@ -113,7 +113,7 @@ export function DimensionContainer({ > {i18n.translate('xpack.lens.configure.configurePanelTitle', { - defaultMessage: '{groupLabel} configuration', + defaultMessage: '{groupLabel}', values: { groupLabel, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index dca8e926646f0..b35986c42054d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -750,7 +750,7 @@ function getErrorMessage( if (selectedColumn && incompleteOperation) { if (input === 'field') { return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', { - defaultMessage: 'To use this function, select a different field.', + defaultMessage: 'This field does not work with the selected function.', }); } return i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', { From 4b25b9d44e99dad0072c8b0fe37735d38bbf8c89 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 12:32:21 -0400 Subject: [PATCH 055/118] [Lens] Clicking number histogram bar applies global filter instead of time filter (#102730) (#102924) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marco Liberati --- .../xy_visualization/expression.test.tsx | 146 ++++++++++++++++++ .../public/xy_visualization/expression.tsx | 8 +- 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index ee1f66063ad1d..930f6888ce532 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -1099,6 +1099,152 @@ describe('xy_expression', () => { }); }); + test('onElementClick returns correct context data for date histogram', () => { + const geometry: GeometryValue = { + x: 1585758120000, + y: 1, + accessor: 'y1', + mark: null, + datum: {}, + }; + const series = { + key: 'spec{d}yAccessor{d}splitAccessors{b-2}', + specId: 'd', + yAccessor: 'yAccessorId', + splitAccessors: {}, + seriesKeys: ['yAccessorId'], + }; + + const { args } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + wrapper.find(Settings).first().prop('onElementClick')!([ + [geometry, series as XYChartSeriesIdentifier], + ]); + + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: dateHistogramData.tables.timeLayer, + value: 1585758120000, + }, + ], + timeFieldName: 'order_date', + }); + }); + + test('onElementClick returns correct context data for numeric histogram', () => { + const { args } = sampleArgs(); + + const numberLayer: LayerArgs = { + layerId: 'numberLayer', + hide: false, + xAccessor: 'xAccessorId', + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: true, + seriesType: 'bar_stacked', + accessors: ['yAccessorId'], + palette: mockPaletteOutput, + }; + + const numberHistogramData: LensMultiTable = { + type: 'lens_multitable', + tables: { + numberLayer: { + type: 'datatable', + rows: [ + { + xAccessorId: 5, + yAccessorId: 1, + }, + { + xAccessorId: 7, + yAccessorId: 1, + }, + { + xAccessorId: 8, + yAccessorId: 1, + }, + { + xAccessorId: 10, + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'bytes', + meta: { type: 'number' }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { type: 'number' }, + }, + ], + }, + }, + dateRange: { + fromDate: new Date('2020-04-01T16:14:16.246Z'), + toDate: new Date('2020-04-01T17:15:41.263Z'), + }, + }; + const geometry: GeometryValue = { + x: 5, + y: 1, + accessor: 'y1', + mark: null, + datum: {}, + }; + const series = { + key: 'spec{d}yAccessor{d}splitAccessors{b-2}', + specId: 'd', + yAccessor: 'yAccessorId', + splitAccessors: {}, + seriesKeys: ['yAccessorId'], + }; + + const wrapper = mountWithIntl( + + ); + + wrapper.find(Settings).first().prop('onElementClick')!([ + [geometry, series as XYChartSeriesIdentifier], + ]); + + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: numberHistogramData.tables.numberLayer, + value: 5, + }, + ], + timeFieldName: undefined, + }); + }); + test('returns correct original data for ordinal x axis with special formatter', () => { const geometry: GeometryValue = { x: 'BAR', y: 1, accessor: 'y1', mark: null, datum: {} }; const series = { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 24842c83c23b1..1de5cf6b30533 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -562,9 +562,9 @@ export function XYChart({ value: pointValue, }); } - - const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta?.field; - const timeFieldName = xDomain && xAxisFieldName; + const currentColumnMeta = table.columns.find((el) => el.id === layer.xAccessor)?.meta; + const xAxisFieldName = currentColumnMeta?.field; + const isDateField = currentColumnMeta?.type === 'date'; const context: LensFilterEvent['data'] = { data: points.map((point) => ({ @@ -573,7 +573,7 @@ export function XYChart({ value: point.value, table, })), - timeFieldName, + timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined, }; onClickValue(desanitizeFilterContext(context)); }; From d9b2a0778284945dc41e6bbdd02ff63423b717d4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 12:41:00 -0400 Subject: [PATCH 056/118] Add "Unable to decrypt attribute apiKey" to the alerting troubleshooting docs (#101315) (#102961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial commit * PR feedback * PR feedback pt 2 * PR feedback pt 3 Co-authored-by: Mike Côté --- .../alerting-troubleshooting.asciidoc | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc index b7b0c749dfe14..08655508b3cba 100644 --- a/docs/user/alerting/alerting-troubleshooting.asciidoc +++ b/docs/user/alerting/alerting-troubleshooting.asciidoc @@ -12,6 +12,32 @@ If your problem isn’t described here, please review open issues in the followi Have a question? Contact us in the https://discuss.elastic.co/[discuss forum]. +[float] +[[rule-cannot-decrypt-api-key]] +=== Rule cannot decrypt apiKey + +*Problem*: + +The rule fails to execute and has an `Unable to decrypt attribute "apiKey"` error. + +*Solution*: + +This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used during rule execution. Depending on the scenario, there are different ways to solve this problem: + +[cols="2*<"] +|=== + +| If the value in `xpack.encryptedSavedObjects.encryptionKey` was manually changed, and the previous encryption key is still known. +| Ensure any previous encryption key is included in the keys used for <>. + +| If another {kib} instance with a different encryption key connects to the cluster. +| The other {kib} instance might be trying to run the rule using a different encryption key than what the rule was created with. Ensure the encryption keys among all the {kib} instances are the same, and setting <> for previously used encryption keys. + +| If other scenarios don't apply. +| Generate a new API key for the rule by disabling then enabling the rule. + +|=== + [float] [[rules-small-check-interval-run-late]] === Rules with small check intervals run late @@ -29,7 +55,6 @@ Either tweak the <> or increa For more details, see <>. - [float] [[scheduled-rules-run-late]] === Rules run late From 67305862c194d03c0a25518b407d2dcf4ad5d410 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 12:59:56 -0400 Subject: [PATCH 057/118] Handle element changing into a filter (#97890) (#102932) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Corey Robertson --- .../renderers/filters/dropdown_filter/index.tsx | 7 +++++-- .../renderers/filters/time_filter/index.tsx | 9 +++++++-- x-pack/plugins/canvas/public/lib/create_handlers.ts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index 97b5e592552ed..fbcba9e56aef5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -45,9 +45,12 @@ export const dropdownFilter: RendererFactory = () => ({ reuseDomNode: true, height: 50, render(domNode, config, handlers) { - const filterExpression = handlers.getFilter(); + let filterExpression = handlers.getFilter(); - if (filterExpression !== '') { + if (filterExpression === undefined || filterExpression.indexOf('exactly')) { + filterExpression = ''; + handlers.setFilter(filterExpression); + } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed const { changed, newAst } = syncFilterExpression(config, filterExpression, ['filterGroup']); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index ff781bb294db4..02a36b80fa364 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -19,6 +19,8 @@ import { RendererFactory } from '../../../../types'; const { timeFilter: strings } = RendererStrings; +const defaultTimeFilterExpression = 'timefilter column=@timestamp from=now-24h to=now'; + export const timeFilterFactory: StartInitializer> = (core, plugins) => { const { uiSettings } = core; @@ -38,9 +40,12 @@ export const timeFilterFactory: StartInitializer> = ( help: strings.getHelpDescription(), reuseDomNode: true, // must be true, otherwise popovers don't work render: async (domNode: HTMLElement, config: Arguments, handlers: RendererHandlers) => { - const filterExpression = handlers.getFilter(); + let filterExpression = handlers.getFilter(); - if (filterExpression !== '') { + if (filterExpression === undefined || filterExpression.indexOf('timefilter') !== 0) { + filterExpression = defaultTimeFilterExpression; + handlers.setFilter(filterExpression); + } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed const { changed, newAst } = syncFilterExpression(config, filterExpression, [ diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 9f531d6921417..13d2a998e4f95 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -105,7 +105,7 @@ export const createDispatchedHandlerFactory = ( }, getFilter() { - return element.filter; + return element.filter || ''; }, onComplete(fn: () => void) { From b911f5df4f28eb5c72e8465351a338f857af0718 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:01:56 -0400 Subject: [PATCH 058/118] Fixes onDestroy handler (#101959) (#102931) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Corey Robertson --- x-pack/plugins/canvas/public/lib/create_handlers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 13d2a998e4f95..aba29ccd542be 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -44,6 +44,10 @@ export const createHandlers = (baseHandlers = createBaseHandlers()): RendererHan this.done = fn; }, + onDestroy(fn: () => void) { + this.destroy = fn; + }, + // TODO: these functions do not match the `onXYZ` and `xyz` pattern elsewhere. onEmbeddableDestroyed() {}, onEmbeddableInputChange() {}, From ba95a28dd8a3d7a5fdefd01ce3a8802d326d48ca Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:14:10 -0400 Subject: [PATCH 059/118] [Fleet] Add assets tab (#102517) (#102936) * very wip * added new assets screen * added routes to new assets view on the package details view * Finished styling the assets page layout, need to work on adding links * rather use EuiHorizontalRule * only show the assets tab if installed * Added hacky version of linking to assets. * added comment about deprecation of current linking functionality * added an initial version of the success toast with a link to the agent flyout * First iteration of end-to-end UX working. Need to add a lot of tests! * fixed navigation bug and added a comment * added a lot more padding to bottom of form * restructured code for clarity, updated deprecation comments and moved relevant code closer together * added a longer form comment about the origin policyId * added logic for handling load error * refactor assets accordions out of assets page component * slightly larger text in badge * added some basic jest test for view data step in enrollment flyout * adjusted sizing of numbers in badges again, EuiText does not know about size="l" * updated size limits for fleet * updated styling and layout of assets accordion based on original designs * remove unused EuiTitle Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jean-Louis Leysens --- packages/kbn-optimizer/limits.yml | 2 +- .../plugins/fleet/common/types/models/epm.ts | 2 +- .../create_package_policy_page/index.tsx | 91 ++++++++-- .../package_policies/no_package_policies.tsx | 12 +- .../epm/screens/detail/assets/assets.tsx | 138 +++++++++++++++ .../detail/assets/assets_accordion.tsx | 92 ++++++++++ .../epm/screens/detail/assets/constants.ts | 16 ++ .../epm/screens/detail/assets/index.ts | 7 + .../epm/screens/detail/assets/types.ts | 20 +++ .../sections/epm/screens/detail/index.tsx | 22 +++ .../detail/policies/package_policies.tsx | 73 ++++++-- .../agent_enrollment_flyout.test.mocks.ts | 1 + .../agent_enrollment_flyout.test.tsx | 42 ++++- .../agent_enrollment_flyout/index.tsx | 13 +- .../managed_instructions.tsx | 167 ++++++++++-------- .../agent_enrollment_flyout/steps.tsx | 13 ++ .../agent_enrollment_flyout/types.ts | 12 +- .../package_policy_actions_menu.tsx | 9 +- .../fleet/public/constants/page_paths.ts | 13 +- x-pack/plugins/fleet/public/hooks/index.ts | 2 +- .../fleet/public/hooks/use_kibana_link.ts | 54 +++++- 21 files changed, 676 insertions(+), 125 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 856d4439f4f6b..2a7f85c485560 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexManagement: 140608 indexPatternManagement: 28222 infra: 184320 - fleet: 450005 + fleet: 465774 ingestPipelines: 58003 inputControlVis: 172819 inspector: 148999 diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 83875801300d3..aece658083196 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -43,7 +43,7 @@ export type InstallSource = 'registry' | 'upload'; export type EpmPackageInstallStatus = 'installed' | 'installing'; -export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom'; +export type DetailViewPanelName = 'overview' | 'policies' | 'assets' | 'settings' | 'custom'; export type ServiceName = 'kibana' | 'elasticsearch'; export type AgentAssetType = typeof agentAssetTypes; export type DocAssetType = 'doc' | 'notice'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 75fc06c1a4494..b3b0d6ed51cb4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -19,10 +19,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, + EuiLink, } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import type { ApplicationStart } from 'kibana/public'; +import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import type { AgentPolicy, PackageInfo, @@ -60,7 +62,7 @@ const StepsWithLessPadding = styled(EuiSteps)` `; const CustomEuiBottomBar = styled(EuiBottomBar)` - // Set a relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar + /* A relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar */ z-index: 50; `; @@ -84,11 +86,26 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { const history = useHistory(); const handleNavigateTo = useNavigateToCallback(); const routeState = useIntraAppState(); - const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package'; const { search } = useLocation(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); - const policyId = useMemo(() => queryParams.get('policyId') ?? undefined, [queryParams]); + const queryParamsPolicyId = useMemo(() => queryParams.get('policyId') ?? undefined, [ + queryParams, + ]); + + /** + * Please note: policyId can come from one of two sources. The URL param (in the URL path) or + * in the query params (?policyId=foo). + * + * Either way, we take this as an indication that a user is "coming from" the fleet policy UI + * since we link them out to packages (a.k.a. integrations) UI when choosing a new package. It is + * no longer possible to choose a package directly in the create package form. + * + * We may want to deprecate the ability to pass in policyId from URL params since there is no package + * creation possible if a user has not chosen one from the packages UI. + */ + const from: CreatePackagePolicyFrom = + 'policyId' in params || queryParamsPolicyId ? 'policy' : 'package'; // Agent policy and package info states const [agentPolicy, setAgentPolicy] = useState(); @@ -280,6 +297,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ); } + const fromPolicyWithoutAgentsAssigned = from === 'policy' && agentPolicy && agentCount === 0; + + const fromPackageWithoutAgentsAssigned = + from === 'package' && packageInfo && agentPolicy && agentCount === 0; + + const hasAgentsAssigned = agentCount && agentPolicy; + notifications.toasts.addSuccess({ title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', { defaultMessage: `'{packagePolicyName}' integration added.`, @@ -287,22 +311,47 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { packagePolicyName: packagePolicy.name, }, }), - text: - agentCount && agentPolicy - ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', { - defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, - values: { - agentPolicyName: agentPolicy.name, - }, - }) - : (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0 - ? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', { + text: fromPolicyWithoutAgentsAssigned + ? i18n.translate( + 'xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage', + { defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`, values: { - agentPolicyName: agentPolicy.name, + agentPolicyName: agentPolicy!.name, }, - }) - : undefined, + } + ) + : fromPackageWithoutAgentsAssigned + ? toMountPoint( + // To render the link below we need to mount this JSX in the success toast + + {i18n.translate( + 'xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage', + { defaultMessage: 'add an agent' } + )} + + ), + }} + /> + ) + : hasAgentsAssigned + ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, + values: { + agentPolicyName: agentPolicy!.name, + }, + }) + : undefined, 'data-test-subj': 'packagePolicyCreateSuccessToast', }); } else { @@ -312,6 +361,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { setFormState('VALID'); } }, [ + getHref, + from, + packageInfo, agentCount, agentPolicy, formState, @@ -353,13 +405,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ), - [params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId] + [params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId] ); const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); @@ -455,7 +507,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { )} - + + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx index 54adbd78ab75a..39340a21d349b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx @@ -9,10 +9,11 @@ import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { useCapabilities, useLink } from '../../../../../hooks'; +import { useCapabilities, useStartServices } from '../../../../../hooks'; +import { pagePathGetters, INTEGRATIONS_PLUGIN_ID } from '../../../../../constants'; export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => { - const { getHref } = useLink(); + const { application } = useStartServices(); const hasWriteCapabilities = useCapabilities().write; return ( @@ -36,7 +37,12 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => { + application.navigateToApp(INTEGRATIONS_PLUGIN_ID, { + path: `#${pagePathGetters.integrations_all()[1]}`, + state: { forAgentPolicyId: policyId }, + }) + } > { + const { name, version } = packageInfo; + const { + savedObjects: { client: savedObjectsClient }, + } = useStartServices(); + + const { getPath } = useLink(); + const getPackageInstallStatus = useGetPackageInstallStatus(); + const packageInstallStatus = getPackageInstallStatus(packageInfo.name); + + const [assetSavedObjects, setAssetsSavedObjects] = useState(); + const [fetchError, setFetchError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchAssetSavedObjects = async () => { + if ('savedObject' in packageInfo) { + const { + savedObject: { attributes: packageAttributes }, + } = packageInfo; + + if ( + !packageAttributes.installed_kibana || + packageAttributes.installed_kibana.length === 0 + ) { + setIsLoading(false); + return; + } + + try { + const objectsToGet = packageAttributes.installed_kibana.map(({ id, type }) => ({ + id, + type, + })); + const { savedObjects } = await savedObjectsClient.bulkGet(objectsToGet); + setAssetsSavedObjects(savedObjects as AssetSavedObject[]); + } catch (e) { + setFetchError(e); + } finally { + setIsLoading(false); + } + } else { + setIsLoading(false); + } + }; + fetchAssetSavedObjects(); + }, [savedObjectsClient, packageInfo]); + + // if they arrive at this page and the package is not installed, send them to overview + // this happens if they arrive with a direct url or they uninstall while on this tab + if (packageInstallStatus.status !== InstallStatus.installed) { + return ( + + ); + } + + let content: JSX.Element | Array; + + if (isLoading) { + content = ; + } else if (fetchError) { + content = ( + + } + error={fetchError} + /> + ); + } else if (assetSavedObjects === undefined) { + content = ( + +

+ +

+
+ ); + } else { + content = allowedAssetTypes.map((assetType) => { + const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType); + + if (!sectionAssetSavedObjects.length) { + return null; + } + + return ( + <> + + + + ); + }); + } + + return ( + + + {content} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx new file mode 100644 index 0000000000000..abfdd88d27162 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx @@ -0,0 +1,92 @@ +/* + * 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 type { FunctionComponent } from 'react'; + +import { + EuiAccordion, + EuiFlexGroup, + EuiFlexItem, + EuiSplitPanel, + EuiSpacer, + EuiText, + EuiLink, + EuiHorizontalRule, + EuiNotificationBadge, +} from '@elastic/eui'; + +import { AssetTitleMap } from '../../../../../constants'; + +import { getHrefToObjectInKibanaApp, useStartServices } from '../../../../../hooks'; + +import type { AllowedAssetType, AssetSavedObject } from './types'; + +interface Props { + type: AllowedAssetType; + savedObjects: AssetSavedObject[]; +} + +export const AssetsAccordion: FunctionComponent = ({ savedObjects, type }) => { + const { http } = useStartServices(); + return ( + + + +

{AssetTitleMap[type]}

+
+
+ + +

{savedObjects.length}

+
+
+
+ } + id={type} + > + <> + + + {savedObjects.map(({ id, attributes: { title, description } }, idx) => { + const pathToObjectInApp = getHrefToObjectInKibanaApp({ + http, + id, + type, + }); + return ( + <> + + +

+ {pathToObjectInApp ? ( + {title} + ) : ( + title + )} +

+
+ {description && ( + <> + + +

{description}

+
+ + )} +
+ {idx + 1 < savedObjects.length && } + + ); + })} +
+ + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts new file mode 100644 index 0000000000000..d6d88f7935eb4 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaAssetType } from '../../../../../types'; + +import type { AllowedAssetTypes } from './types'; + +export const allowedAssetTypes: AllowedAssetTypes = [ + KibanaAssetType.dashboard, + KibanaAssetType.search, + KibanaAssetType.visualization, +]; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts new file mode 100644 index 0000000000000..ceb030b7ce02e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { AssetsPage } from './assets'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts new file mode 100644 index 0000000000000..21efd1cd562e8 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.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 type { SimpleSavedObject } from 'src/core/public'; + +import type { KibanaAssetType } from '../../../../../types'; + +export type AssetSavedObject = SimpleSavedObject<{ title: string; description?: string }>; + +export type AllowedAssetTypes = [ + KibanaAssetType.dashboard, + KibanaAssetType.search, + KibanaAssetType.visualization +]; + +export type AllowedAssetType = AllowedAssetTypes[number]; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 99a29a8194f9b..cf6007026afeb 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -56,6 +56,7 @@ import { WithHeaderLayout } from '../../../../layouts'; import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge'; import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components'; +import { AssetsPage } from './assets'; import { OverviewPage } from './overview'; import { PackagePoliciesPage } from './policies'; import { SettingsPage } from './settings'; @@ -408,6 +409,24 @@ export function Detail() { }); } + if (packageInstallStatus === InstallStatus.installed && packageInfo.assets) { + tabs.push({ + id: 'assets', + name: ( + + ), + isSelected: panel === 'assets', + 'data-test-subj': `tab-assets`, + href: getHref('integration_details_assets', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }), + }); + } + tabs.push({ id: 'settings', name: ( @@ -476,6 +495,9 @@ export function Detail() { + + + diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 7da7328fdebbc..c672abeb1c903 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { stringify, parse } from 'query-string'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import { Redirect } from 'react-router-dom'; +import { Redirect, useLocation, useHistory } from 'react-router-dom'; import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui'; import { EuiButtonIcon, @@ -15,6 +15,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip, + EuiText, + EuiButton, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; @@ -66,8 +69,16 @@ interface PackagePoliciesPanelProps { version: string; } export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { - const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(null); - const { getPath } = useLink(); + const { search } = useLocation(); + const history = useHistory(); + const queryParams = useMemo(() => new URLSearchParams(search), [search]); + const agentPolicyIdFromParams = useMemo(() => queryParams.get('addAgentToPolicyId'), [ + queryParams, + ]); + const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState( + agentPolicyIdFromParams + ); + const { getPath, getHref } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); @@ -87,6 +98,36 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps [setPagination] ); + const renderViewDataStepContent = useCallback( + () => ( + <> + + + {i18n.translate( + 'xpack.fleet.epm.agentEnrollment.viewDataDescription.pleaseNoteLabel', + { defaultMessage: 'Please note' } + )} +
+ ), + }} + /> + + + + {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', { + defaultMessage: 'View assets', + })} + + + ), + [name, version, getHref] + ); + const columns: Array> = useMemo( () => [ { @@ -186,12 +227,16 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps align: 'right', render({ agentPolicy, packagePolicy }) { return ( - + ); }, }, ], - [] + [renderViewDataStepContent] ); const noItemsMessage = useMemo(() => { @@ -236,14 +281,18 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps /> - {flyoutOpenForPolicyId && ( + {flyoutOpenForPolicyId && !isLoading && ( setFlyoutOpenForPolicyId(null)} - agentPolicies={ - data?.items - .filter(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId) - .map(({ agentPolicy }) => agentPolicy) ?? [] + onClose={() => { + setFlyoutOpenForPolicyId(null); + const { addAgentToPolicyId, ...rest } = parse(search); + history.replace({ search: stringify(rest) }); + }} + agentPolicy={ + data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId) + ?.agentPolicy } + viewDataStepContent={renderViewDataStepContent()} /> )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts index f1055e7e2583e..fcf1078566498 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts @@ -37,6 +37,7 @@ jest.mock('./steps', () => { ...module, AgentPolicySelectionStep: jest.fn(), AgentEnrollmentKeySelectionStep: jest.fn(), + ViewDataStep: jest.fn(), }; }); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx index db9245b11b0f9..65118044e98c5 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx @@ -21,7 +21,7 @@ import { FleetStatusProvider, ConfigContext } from '../../hooks'; import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page'; -import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps'; +import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps'; import type { Props } from '.'; import { AgentEnrollmentFlyout } from '.'; @@ -128,6 +128,46 @@ describe('', () => { expect(AgentEnrollmentKeySelectionStep).toHaveBeenCalled(); }); }); + + describe('"View data" extension point', () => { + it('calls the "View data" step when UI extension is provided', async () => { + jest.clearAllMocks(); + await act(async () => { + testBed = await setup({ + agentPolicies: [], + onClose: jest.fn(), + viewDataStepContent:
, + }); + testBed.component.update(); + }); + const { exists, actions } = testBed; + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(ViewDataStep).toHaveBeenCalled(); + + jest.clearAllMocks(); + actions.goToStandaloneTab(); + expect(ViewDataStep).not.toHaveBeenCalled(); + }); + + it('does not call the "View data" step when UI extension is not provided', async () => { + jest.clearAllMocks(); + await act(async () => { + testBed = await setup({ + agentPolicies: [], + onClose: jest.fn(), + viewDataStepContent: undefined, + }); + testBed.component.update(); + }); + const { exists, actions } = testBed; + expect(exists('agentEnrollmentFlyout')).toBe(true); + expect(ViewDataStep).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + actions.goToStandaloneTab(); + expect(ViewDataStep).not.toHaveBeenCalled(); + }); + }); }); describe('standalone instructions', () => { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index b91af80691033..58362d85e2fb3 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -42,6 +42,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentPolicy, agentPolicies, + viewDataStepContent, }) => { const [mode, setMode] = useState<'managed' | 'standalone'>('managed'); @@ -109,9 +110,17 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ } > {fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? ( - + ) : ( - + )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index e7045173f1257..919f0c3052db9 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -21,7 +21,12 @@ import { useFleetServerInstructions, } from '../../applications/fleet/sections/agents/agent_requirements_page'; -import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps'; +import { + DownloadStep, + AgentPolicySelectionStep, + AgentEnrollmentKeySelectionStep, + ViewDataStep, +} from './steps'; import type { BaseProps } from './types'; type Props = BaseProps; @@ -53,83 +58,91 @@ const FleetServerMissingRequirements = () => { return ; }; -export const ManagedInstructions = React.memo(({ agentPolicy, agentPolicies }) => { - const fleetStatus = useFleetStatus(); +export const ManagedInstructions = React.memo( + ({ agentPolicy, agentPolicies, viewDataStepContent }) => { + const fleetStatus = useFleetStatus(); - const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); - const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); + const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); + const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); - const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); - const settings = useGetSettings(); - const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); + const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); + const settings = useGetSettings(); + const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); - const steps = useMemo(() => { - const { - serviceToken, - getServiceToken, - isLoadingServiceToken, - installCommand, - platform, - setPlatform, - } = fleetServerInstructions; - const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; - const baseSteps: EuiContainedStepProps[] = [ - DownloadStep(), - !agentPolicy - ? AgentPolicySelectionStep({ - agentPolicies, - setSelectedAPIKeyId, - setIsFleetServerPolicySelected, - }) - : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }), - ]; - if (isFleetServerPolicySelected) { - baseSteps.push( - ...[ - ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }), - FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }), - ] - ); - } else { - baseSteps.push({ - title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { - defaultMessage: 'Enroll and start the Elastic Agent', - }), - children: selectedAPIKeyId && apiKey.data && ( - - ), - }); - } - return baseSteps; - }, [ - agentPolicy, - agentPolicies, - selectedAPIKeyId, - apiKey.data, - isFleetServerPolicySelected, - settings.data?.item?.fleet_server_hosts, - fleetServerInstructions, - ]); + const steps = useMemo(() => { + const { + serviceToken, + getServiceToken, + isLoadingServiceToken, + installCommand, + platform, + setPlatform, + } = fleetServerInstructions; + const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; + const baseSteps: EuiContainedStepProps[] = [ + DownloadStep(), + !agentPolicy + ? AgentPolicySelectionStep({ + agentPolicies, + setSelectedAPIKeyId, + setIsFleetServerPolicySelected, + }) + : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }), + ]; + if (isFleetServerPolicySelected) { + baseSteps.push( + ...[ + ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }), + FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }), + ] + ); + } else { + baseSteps.push({ + title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { + defaultMessage: 'Enroll and start the Elastic Agent', + }), + children: selectedAPIKeyId && apiKey.data && ( + + ), + }); + } - return ( - <> - {fleetStatus.isReady ? ( - <> - - - - - - - ) : fleetStatus.missingRequirements?.length === 1 && - fleetStatus.missingRequirements[0] === 'fleet_server' ? ( - - ) : ( - - )} - - ); -}); + if (viewDataStepContent) { + baseSteps.push(ViewDataStep(viewDataStepContent)); + } + + return baseSteps; + }, [ + agentPolicy, + agentPolicies, + selectedAPIKeyId, + apiKey.data, + isFleetServerPolicySelected, + settings.data?.item?.fleet_server_hosts, + fleetServerInstructions, + viewDataStepContent, + ]); + + return ( + <> + {fleetStatus.isReady ? ( + <> + + + + + + + ) : fleetStatus.missingRequirements?.length === 1 && + fleetStatus.missingRequirements[0] === 'fleet_server' ? ( + + ) : ( + + )} + + ); + } +); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index ea4fa626afbb6..03cff88e63969 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -138,3 +138,16 @@ export const AgentEnrollmentKeySelectionStep = ({ ), }; }; + +/** + * Send users to assets installed by the package in Kibana so they can + * view their data. + */ +export const ViewDataStep = (content: JSX.Element) => { + return { + title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', { + defaultMessage: 'View your data', + }), + children: content, + }; +}; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index b9bcf8fb3e4b2..e0c5b040a61fb 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -9,12 +9,20 @@ import type { AgentPolicy } from '../../types'; export interface BaseProps { /** - * The user selected policy to be used + * The user selected policy to be used. If this value is `undefined` a value must be provided for `agentPolicies`. */ agentPolicy?: AgentPolicy; /** - * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided + * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided. + * + * If this value is `undefined` a value must be provided for `agentPolicy`. */ agentPolicies?: AgentPolicy[]; + + /** + * There is a step in the agent enrollment process that allows users to see the data from an integration represented in the UI + * in some way. This is an area for consumers to render a button and text explaining how data can be viewed. + */ + viewDataStepContent?: JSX.Element; } diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 03bf2095f7f3e..1f64de27fce39 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -21,7 +21,8 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export const PackagePolicyActionsMenu: React.FunctionComponent<{ agentPolicy: AgentPolicy; packagePolicy: PackagePolicy; -}> = ({ agentPolicy, packagePolicy }) => { + viewDataStepContent?: JSX.Element; +}> = ({ agentPolicy, packagePolicy, viewDataStepContent }) => { const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; @@ -103,7 +104,11 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ <> {isEnrollmentFlyoutOpen && ( - + )} diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index 326cfd804bd57..1688a396cd5a1 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { stringify } from 'query-string'; + export type StaticPage = | 'base' | 'overview' @@ -19,6 +21,7 @@ export type StaticPage = export type DynamicPage = | 'integration_details_overview' | 'integration_details_policies' + | 'integration_details_assets' | 'integration_details_settings' | 'integration_details_custom' | 'integration_policy_edit' @@ -66,6 +69,7 @@ export const INTEGRATIONS_ROUTING_PATHS = { integration_details: '/detail/:pkgkey/:panel?', integration_details_overview: '/detail/:pkgkey/overview', integration_details_policies: '/detail/:pkgkey/policies', + integration_details_assets: '/detail/:pkgkey/assets', integration_details_settings: '/detail/:pkgkey/settings', integration_details_custom: '/detail/:pkgkey/custom', integration_policy_edit: '/edit-integration/:packagePolicyId', @@ -86,9 +90,13 @@ export const pagePathGetters: { INTEGRATIONS_BASE_PATH, `/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`, ], - integration_details_policies: ({ pkgkey, integration }) => [ + integration_details_policies: ({ pkgkey, integration, addAgentToPolicyId }) => { + const qs = stringify({ integration, addAgentToPolicyId }); + return [INTEGRATIONS_BASE_PATH, `/detail/${pkgkey}/policies${qs ? `?${qs}` : ''}`]; + }, + integration_details_assets: ({ pkgkey, integration }) => [ INTEGRATIONS_BASE_PATH, - `/detail/${pkgkey}/policies${integration ? `?integration=${integration}` : ''}`, + `/detail/${pkgkey}/assets${integration ? `?integration=${integration}` : ''}`, ], integration_details_settings: ({ pkgkey, integration }) => [ INTEGRATIONS_BASE_PATH, @@ -108,6 +116,7 @@ export const pagePathGetters: { FLEET_BASE_PATH, `/policies/${policyId}${tabId ? `/${tabId}` : ''}`, ], + // TODO: This might need to be removed because we do not have a way to pick an integration in line anymore add_integration_from_policy: ({ policyId }) => [ FLEET_BASE_PATH, `/policies/${policyId}/add-integration`, diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index 9f41e5c7cc92b..a00c0c5dacf11 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -11,7 +11,7 @@ export { useConfig, ConfigContext } from './use_config'; export { useKibanaVersion, KibanaVersionContext } from './use_kibana_version'; export { licenseService, useLicense } from './use_license'; export { useLink } from './use_link'; -export { useKibanaLink } from './use_kibana_link'; +export { useKibanaLink, getHrefToObjectInKibanaApp } from './use_kibana_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination, PAGE_SIZE_OPTIONS } from './use_pagination'; export { useUrlPagination } from './use_url_pagination'; diff --git a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts index 29f4f8748d1a0..3ad01620b9780 100644 --- a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts +++ b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts @@ -4,12 +4,62 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { HttpStart } from 'src/core/public'; + +import { KibanaAssetType } from '../types'; import { useStartServices } from './'; const KIBANA_BASE_PATH = '/app/kibana'; +const getKibanaLink = (http: HttpStart, path: string) => { + return http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); +}; + +/** + * TODO: This is a temporary solution for getting links to various assets. It is very risky because: + * + * 1. The plugin might not exist/be enabled + * 2. URLs and paths might not always be supported + * + * We should migrate to using the new URL service locators. + * + * @deprecated {@link Locators} from the new URL service need to be used instead. + + */ +export const getHrefToObjectInKibanaApp = ({ + type, + id, + http, +}: { + type: KibanaAssetType; + id: string; + http: HttpStart; +}): undefined | string => { + let kibanaAppPath: undefined | string; + switch (type) { + case KibanaAssetType.dashboard: + kibanaAppPath = `/dashboard/${id}`; + break; + case KibanaAssetType.search: + kibanaAppPath = `/discover/${id}`; + break; + case KibanaAssetType.visualization: + kibanaAppPath = `/visualize/edit/${id}`; + break; + default: + return undefined; + } + + return getKibanaLink(http, kibanaAppPath); +}; + +/** + * TODO: This functionality needs to be replaced with use of the new URL service locators + * + * @deprecated {@link Locators} from the new URL service need to be used instead. + */ export function useKibanaLink(path: string = '/') { - const core = useStartServices(); - return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); + const { http } = useStartServices(); + return getKibanaLink(http, path); } From 9070345f4522c68aee844603fcd2054f73465cca Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:29:47 -0400 Subject: [PATCH 060/118] Wraps query in parentheses to avoid quering exception lists (#102612) (#102941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: David Sánchez --- .../public/management/common/utils.test.ts | 8 ++++---- .../security_solution/public/management/common/utils.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/common/utils.test.ts b/x-pack/plugins/security_solution/public/management/common/utils.test.ts index 59455ccd6bb04..30354c141f833 100644 --- a/x-pack/plugins/security_solution/public/management/common/utils.test.ts +++ b/x-pack/plugins/security_solution/public/management/common/utils.test.ts @@ -15,17 +15,17 @@ describe('utils', () => { }); it('should parse simple query with term', () => { expect(parseQueryFilterToKQL('simpleQuery', searchableFields)).toBe( - 'exception-list-agnostic.attributes.name:(*simpleQuery*) OR exception-list-agnostic.attributes.description:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.value:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.entries.value:(*simpleQuery*)' + '(exception-list-agnostic.attributes.name:(*simpleQuery*) OR exception-list-agnostic.attributes.description:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.value:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.entries.value:(*simpleQuery*))' ); }); it('should parse complex query with term', () => { expect(parseQueryFilterToKQL('complex query', searchableFields)).toBe( - 'exception-list-agnostic.attributes.name:(*complex*query*) OR exception-list-agnostic.attributes.description:(*complex*query*) OR exception-list-agnostic.attributes.entries.value:(*complex*query*) OR exception-list-agnostic.attributes.entries.entries.value:(*complex*query*)' + '(exception-list-agnostic.attributes.name:(*complex*query*) OR exception-list-agnostic.attributes.description:(*complex*query*) OR exception-list-agnostic.attributes.entries.value:(*complex*query*) OR exception-list-agnostic.attributes.entries.entries.value:(*complex*query*))' ); }); it('should parse complex query with colon and backslash chars term', () => { expect(parseQueryFilterToKQL('C:\\tmpes', searchableFields)).toBe( - 'exception-list-agnostic.attributes.name:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.description:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.value:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.entries.value:(*C\\:\\\\tmpes*)' + '(exception-list-agnostic.attributes.name:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.description:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.value:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.entries.value:(*C\\:\\\\tmpes*))' ); }); it('should parse complex query with special chars term', () => { @@ -35,7 +35,7 @@ describe('utils', () => { searchableFields ) ).toBe( - "exception-list-agnostic.attributes.name:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.description:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*)" + "(exception-list-agnostic.attributes.name:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.description:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*))" ); }); }); diff --git a/x-pack/plugins/security_solution/public/management/common/utils.ts b/x-pack/plugins/security_solution/public/management/common/utils.ts index c8cf761ccaf86..616e395c8ad47 100644 --- a/x-pack/plugins/security_solution/public/management/common/utils.ts +++ b/x-pack/plugins/security_solution/public/management/common/utils.ts @@ -17,5 +17,5 @@ export const parseQueryFilterToKQL = (filter: string, fields: Readonly ) .join(' OR '); - return kuery; + return `(${kuery})`; }; From 119a7a0d85030ea0100c3dbc2670f8170f7cef43 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:36:46 -0400 Subject: [PATCH 061/118] [Agent Packages] Extend 'contains' helper to work on strings (#102786) (#102943) * Extend 'contains' helper to work on strings * remove stray import Co-authored-by: Andrew Stucki --- .../server/services/epm/agent/agent.test.ts | 29 +++++++++++++++++++ .../fleet/server/services/epm/agent/agent.ts | 5 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index bc4ffffb68358..1be0f73a347e9 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -129,6 +129,13 @@ processors: password: {{password}} {{#if password}} hidden_password: {{password}} +{{/if}} + `; + const streamTemplateWithString = ` +{{#if (contains ".pcap" file)}} +pcap: true +{{else}} +pcap: false {{/if}} `; @@ -168,6 +175,28 @@ hidden_password: {{password}} tags: ['foo', 'bar'], }); }); + + it('should support strings', () => { + const vars = { + file: { value: 'foo.pcap' }, + }; + + const output = compileTemplate(vars, streamTemplateWithString); + expect(output).toEqual({ + pcap: true, + }); + }); + + it('should support strings with no match', () => { + const vars = { + file: { value: 'file' }, + }; + + const output = compileTemplate(vars, streamTemplateWithString); + expect(output).toEqual({ + pcap: false, + }); + }); }); it('should support optional yaml values at root level', () => { diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index 84a8ab581354a..a0d14e6962a8d 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -111,11 +111,12 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt return { vars, yamlValues }; } -function containsHelper(this: any, item: string, list: string[], options: any) { - if (Array.isArray(list) && list.includes(item)) { +function containsHelper(this: any, item: string, check: string | string[], options: any) { + if ((Array.isArray(check) || typeof check === 'string') && check.includes(item)) { if (options && options.fn) { return options.fn(this); } + return true; } return ''; } From d16023c72245bdf782091934b60b5b0c9c794bdb Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:45:25 -0400 Subject: [PATCH 062/118] [App Search] Convert Analytics views to new page template (#102851) (#102945) * Convert AnalyticsHeader to AnalyticsFilters - it's basically the same component as before, but without the title section/log retention tooltip, since the header/title will be handled by the new page template * Update AnalyticsLayout to use new page template + add new test_helper for header children * Update breadcrumb behavior - Set analytic breadcrumbs in AnalyticsLayout rather than AnalyticsRouter - Update individual views to pass breadcrumbs (consistent with new page template API) * Update router Co-authored-by: Constance --- .../analytics/analytics_layout.test.tsx | 28 ++-- .../components/analytics/analytics_layout.tsx | 34 +++-- .../components/analytics/analytics_router.tsx | 25 +--- ...er.test.tsx => analytics_filters.test.tsx} | 24 ++-- .../components/analytics_filters.tsx | 111 ++++++++++++++ .../components/analytics_header.scss | 15 -- .../analytics/components/analytics_header.tsx | 136 ------------------ .../components/analytics/components/index.ts | 2 +- .../analytics/views/query_detail.test.tsx | 16 +-- .../analytics/views/query_detail.tsx | 11 +- .../analytics/views/recent_queries.tsx | 2 +- .../analytics/views/top_queries.tsx | 2 +- .../analytics/views/top_queries_no_clicks.tsx | 6 +- .../views/top_queries_no_results.tsx | 6 +- .../views/top_queries_with_clicks.tsx | 6 +- .../components/engine/engine_router.tsx | 10 +- .../test_helpers/get_page_header.tsx | 6 + .../public/applications/test_helpers/index.ts | 1 + 18 files changed, 199 insertions(+), 242 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/{analytics_header.test.tsx => analytics_filters.test.tsx} (87%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx 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 9832915f19e9e..280282a2fc6ec 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 @@ -8,18 +8,22 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { mockKibanaValues, setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; import { mockUseParams } from '../../../__mocks__/react_router'; +import '../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { Loading } from '../../../shared/loading'; -import { rerender } from '../../../test_helpers'; -import { LogRetentionCallout } from '../log_retention'; +import { + rerender, + getPageTitle, + getPageHeaderActions, + getPageHeaderChildren, +} from '../../../test_helpers'; +import { LogRetentionTooltip, LogRetentionCallout } from '../log_retention'; import { AnalyticsLayout } from './analytics_layout'; -import { AnalyticsHeader } from './components'; +import { AnalyticsFilters } from './components'; describe('AnalyticsLayout', () => { const { history } = mockKibanaValues; @@ -47,18 +51,20 @@ describe('AnalyticsLayout', () => { ); - expect(wrapper.find(FlashMessages)).toHaveLength(1); expect(wrapper.find(LogRetentionCallout)).toHaveLength(1); + expect(getPageHeaderActions(wrapper).find(LogRetentionTooltip)).toHaveLength(1); + expect(getPageHeaderChildren(wrapper).find(AnalyticsFilters)).toHaveLength(1); - expect(wrapper.find(AnalyticsHeader).prop('title')).toEqual('Hello'); + expect(getPageTitle(wrapper)).toEqual('Hello'); expect(wrapper.find('[data-test-subj="world"]').text()).toEqual('World!'); + + expect(wrapper.prop('pageChrome')).toEqual(['Engines', 'some-engine', 'Analytics']); }); - it('renders a loading component if data is not done loading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); + it('passes analytics breadcrumbs', () => { + const wrapper = shallow(); - expect(wrapper.type()).toEqual(Loading); + expect(wrapper.prop('pageChrome')).toEqual(['Engines', 'some-engine', 'Analytics', 'Queries']); }); describe('data loading', () => { 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 91de4cc498988..0923f9497a8fe 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 @@ -10,25 +10,27 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiSpacer } from '@elastic/eui'; - -import { FlashMessages } from '../../../shared/flash_messages'; import { KibanaLogic } from '../../../shared/kibana'; -import { Loading } from '../../../shared/loading'; +import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; -import { LogRetentionCallout, LogRetentionOptions } from '../log_retention'; +import { LogRetentionTooltip, LogRetentionCallout, LogRetentionOptions } from '../log_retention'; -import { AnalyticsHeader } from './components'; +import { AnalyticsFilters } from './components'; +import { ANALYTICS_TITLE } from './constants'; import { AnalyticsLogic } from './'; interface Props { title: string; + breadcrumbs?: BreadcrumbTrail; isQueryView?: boolean; isAnalyticsView?: boolean; } export const AnalyticsLayout: React.FC = ({ title, + breadcrumbs = [], isQueryView, isAnalyticsView, children, @@ -43,15 +45,21 @@ export const AnalyticsLayout: React.FC = ({ if (isAnalyticsView) loadAnalyticsData(); }, [history.location.search]); - if (dataLoading) return ; - return ( - <> - - + , + ], + children: , + responsive: false, + }} + isLoading={dataLoading} + > {children} - - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx index 397f1f1e1e1c3..d56fe949431c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { NotFound } from '../../../shared/not_found'; import { ENGINE_ANALYTICS_PATH, @@ -23,14 +22,7 @@ import { } from '../../routes'; import { generateEnginePath, getEngineBreadcrumbs } from '../engine'; -import { - ANALYTICS_TITLE, - TOP_QUERIES, - TOP_QUERIES_NO_RESULTS, - TOP_QUERIES_NO_CLICKS, - TOP_QUERIES_WITH_CLICKS, - RECENT_QUERIES, -} from './constants'; +import { ANALYTICS_TITLE } from './constants'; import { Analytics, TopQueries, @@ -42,42 +34,37 @@ import { } from './views'; export const AnalyticsRouter: React.FC = () => { - const ANALYTICS_BREADCRUMB = getEngineBreadcrumbs([ANALYTICS_TITLE]); - return ( - - - - - - - + - + ); 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_filters.test.tsx similarity index 87% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.test.tsx index 5269ea9110065..7abb02110e2d9 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_filters.test.tsx @@ -12,15 +12,13 @@ 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 { EuiSelect, EuiDatePickerRange, EuiButton } from '@elastic/eui'; import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../constants'; -import { AnalyticsHeader } from './'; +import { AnalyticsFilters } from './'; -describe('AnalyticsHeader', () => { +describe('AnalyticsFilters', () => { const { history } = mockKibanaValues; const values = { @@ -45,18 +43,14 @@ describe('AnalyticsHeader', () => { }); it('renders', () => { - wrapper = shallow(); - - expect(wrapper.type()).toEqual(EuiPageHeader); - expect(wrapper.find('h1').text()).toEqual('Hello world'); + wrapper = shallow(); - expect(wrapper.find(LogRetentionTooltip)).toHaveLength(1); expect(wrapper.find(EuiSelect)).toHaveLength(1); expect(wrapper.find(EuiDatePickerRange)).toHaveLength(1); }); it('renders tags & dates with default values when no search query params are present', () => { - wrapper = shallow(); + wrapper = shallow(); expect(getTagsSelect().prop('value')).toEqual(''); expect(getStartDatePicker().props.startDate._i).toEqual(DEFAULT_START_DATE); @@ -69,7 +63,7 @@ describe('AnalyticsHeader', () => { const allTags = [...values.allTags, 'tag1', 'tag2', 'tag3']; setMockValues({ ...values, allTags }); - wrapper = shallow(); + wrapper = shallow(); }); it('renders the tags select with currentTag value and allTags options', () => { @@ -95,7 +89,7 @@ describe('AnalyticsHeader', () => { beforeEach(() => { history.location.search = '?start=1970-01-01&end=1970-01-02'; - wrapper = shallow(); + wrapper = shallow(); }); it('renders the start date picker', () => { @@ -127,7 +121,7 @@ describe('AnalyticsHeader', () => { beforeEach(() => { history.location.search = '?start=1970-01-02&end=1970-01-01'; - wrapper = shallow(); + wrapper = shallow(); }); it('renders the date pickers as invalid', () => { @@ -148,7 +142,7 @@ describe('AnalyticsHeader', () => { }; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('pushes up new tag & date state to the search query', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx new file mode 100644 index 0000000000000..0c8455e986ae1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx @@ -0,0 +1,111 @@ +/* + * 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 { useValues } from 'kea'; +import moment from 'moment'; +import queryString from 'query-string'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSelect, + EuiDatePickerRange, + EuiDatePicker, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AnalyticsLogic } from '../'; +import { KibanaLogic } from '../../../../shared/kibana'; + +import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants'; +import { convertTagsToSelectOptions } from '../utils'; + +export const AnalyticsFilters: React.FC = () => { + const { allTags } = useValues(AnalyticsLogic); + const { history } = useValues(KibanaLogic); + + // Parse out existing filters from URL query string + const { start, end, tag } = queryString.parse(history.location.search); + const [startDate, setStartDate] = useState( + start ? moment(start, SERVER_DATE_FORMAT) : moment(DEFAULT_START_DATE) + ); + const [endDate, setEndDate] = useState( + end ? moment(end, SERVER_DATE_FORMAT) : moment(DEFAULT_END_DATE) + ); + const [currentTag, setCurrentTag] = useState((tag as string) || ''); + + // Set the current URL query string on filter + const onApplyFilters = () => { + const search = queryString.stringify({ + start: moment(startDate).format(SERVER_DATE_FORMAT), + end: moment(endDate).format(SERVER_DATE_FORMAT), + tag: currentTag || undefined, + }); + history.push({ search }); + }; + + const hasInvalidDateRange = startDate > endDate; + + return ( + + + setCurrentTag(e.target.value)} + aria-label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.tagAriaLabel', + { defaultMessage: 'Filter by analytics tag"' } + )} + fullWidth + /> + + + date && setStartDate(date)} + startDate={startDate} + endDate={endDate} + isInvalid={hasInvalidDateRange} + aria-label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.startDateAriaLabel', + { defaultMessage: 'Filter by start date' } + )} + /> + } + endDateControl={ + date && setEndDate(date)} + startDate={startDate} + endDate={endDate} + isInvalid={hasInvalidDateRange} + aria-label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.endDateAriaLabel', + { defaultMessage: 'Filter by end date' } + )} + /> + } + fullWidth + /> + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.applyButtonLabel', + { defaultMessage: 'Apply filters' } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss deleted file mode 100644 index abe6c0e0789a8..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss +++ /dev/null @@ -1,15 +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. - */ - -.analyticsHeader { - flex-wrap: wrap; - - &__filters.euiPageHeaderSection { - width: 100%; - margin: $euiSizeM 0; - } -} 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 deleted file mode 100644 index 8a87a5e8c211c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx +++ /dev/null @@ -1,136 +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, { useState } from 'react'; - -import { useValues } from 'kea'; -import moment from 'moment'; -import queryString from 'query-string'; - -import { - EuiPageHeader, - EuiPageHeaderSection, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiSelect, - EuiDatePickerRange, - 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 { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants'; -import { convertTagsToSelectOptions } from '../utils'; - -import './analytics_header.scss'; - -interface Props { - title: string; -} -export const AnalyticsHeader: React.FC = ({ title }) => { - const { allTags } = useValues(AnalyticsLogic); - const { history } = useValues(KibanaLogic); - - // Parse out existing filters from URL query string - const { start, end, tag } = queryString.parse(history.location.search); - const [startDate, setStartDate] = useState( - start ? moment(start, SERVER_DATE_FORMAT) : moment(DEFAULT_START_DATE) - ); - const [endDate, setEndDate] = useState( - end ? moment(end, SERVER_DATE_FORMAT) : moment(DEFAULT_END_DATE) - ); - const [currentTag, setCurrentTag] = useState((tag as string) || ''); - - // Set the current URL query string on filter - const onApplyFilters = () => { - const search = queryString.stringify({ - start: moment(startDate).format(SERVER_DATE_FORMAT), - end: moment(endDate).format(SERVER_DATE_FORMAT), - tag: currentTag || undefined, - }); - history.push({ search }); - }; - - const hasInvalidDateRange = startDate > endDate; - - return ( - - - - - -

{title}

-
-
- - - -
-
- - - - setCurrentTag(e.target.value)} - aria-label={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.tagAriaLabel', - { defaultMessage: 'Filter by analytics tag"' } - )} - fullWidth - /> - - - date && setStartDate(date)} - startDate={startDate} - endDate={endDate} - isInvalid={hasInvalidDateRange} - aria-label={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.startDateAriaLabel', - { defaultMessage: 'Filter by start date' } - )} - /> - } - endDateControl={ - date && setEndDate(date)} - startDate={startDate} - endDate={endDate} - isInvalid={hasInvalidDateRange} - aria-label={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.endDateAriaLabel', - { defaultMessage: 'Filter by end date' } - )} - /> - } - fullWidth - /> - - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.applyButtonLabel', - { defaultMessage: 'Apply filters' } - )} - - - - -
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts index de5c8209d2347..5309681b80d6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts @@ -7,7 +7,7 @@ export { AnalyticsCards } from './analytics_cards'; export { AnalyticsChart } from './analytics_chart'; -export { AnalyticsHeader } from './analytics_header'; +export { AnalyticsFilters } from './analytics_filters'; export { AnalyticsSection } from './analytics_section'; export { AnalyticsSearch } from './analytics_search'; export { AnalyticsTable, RecentQueriesTable, QueryClicksTable } from './analytics_tables'; 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 a942918fa9c62..f3fee2553d2fd 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 @@ -12,16 +12,12 @@ import React from 'react'; 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', () => { - const mockBreadcrumbs = ['Engines', 'some-engine', 'Analytics']; - beforeEach(() => { mockUseParams.mockReturnValue({ query: 'some-query' }); @@ -32,16 +28,10 @@ describe('QueryDetail', () => { }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('"some-query"'); - expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([ - 'Engines', - 'some-engine', - 'Analytics', - 'Query', - 'some-query', - ]); + expect(wrapper.find(AnalyticsLayout).prop('breadcrumbs')).toEqual(['Query', 'some-query']); expect(wrapper.find(AnalyticsCards)).toHaveLength(1); expect(wrapper.find(AnalyticsChart)).toHaveLength(1); @@ -50,7 +40,7 @@ describe('QueryDetail', () => { it('renders empty "" search titles correctly', () => { mockUseParams.mockReturnValue({ query: '""' }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('""'); }); 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 83c83aa36f1bb..e68984459cf10 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 @@ -12,8 +12,6 @@ import { useValues } from 'kea'; import { EuiPanel, 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'; import { useDecodedParams } from '../../../utils/encode_path_params'; import { AnalyticsLayout } from '../analytics_layout'; @@ -25,10 +23,7 @@ const QUERY_DETAIL_TITLE = i18n.translate( { defaultMessage: 'Query' } ); -interface Props { - breadcrumbs: BreadcrumbTrail; -} -export const QueryDetail: React.FC = ({ breadcrumbs }) => { +export const QueryDetail: React.FC = () => { const { query } = useDecodedParams(); const queryTitle = query === '""' ? query : `"${query}"`; @@ -37,9 +32,7 @@ export const QueryDetail: React.FC = ({ breadcrumbs }) => { ); return ( - - - + { const { recentQueries } = useValues(AnalyticsLogic); return ( - + 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 6459126560b3a..81b3d08770be6 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 @@ -18,7 +18,7 @@ export const TopQueries: React.FC = () => { const { topQueries } = useValues(AnalyticsLogic); return ( - + 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 8e2591697feaa..2aec88bd372fe 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 @@ -18,7 +18,11 @@ export const TopQueriesNoClicks: React.FC = () => { const { topQueriesNoClicks } = useValues(AnalyticsLogic); return ( - + 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 e093a5130d204..835b259330c83 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 @@ -18,7 +18,11 @@ export const TopQueriesNoResults: React.FC = () => { const { topQueriesNoResults } = useValues(AnalyticsLogic); return ( - + 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 87e276a8382c3..9bea265df55ae 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 @@ -18,7 +18,11 @@ export const TopQueriesWithClicks: React.FC = () => { const { topQueriesWithClicks } = useValues(AnalyticsLogic); return ( - + 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 98627950016fb..b390b1a52b927 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 @@ -94,6 +94,11 @@ export const EngineRouter: React.FC = () => { + {canViewEngineAnalytics && ( + + + + )} {canViewEngineDocuments && ( @@ -106,11 +111,6 @@ export const EngineRouter: React.FC = () => { )} {/* TODO: Remove layout once page template migration is over */} }> - {canViewEngineAnalytics && ( - - - - )} {canViewEngineSchema && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx index 6e89274dca570..a251188b5cd90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx @@ -41,3 +41,9 @@ export const getPageHeaderActions = (wrapper: ShallowWrapper) => {
); }; + +export const getPageHeaderChildren = (wrapper: ShallowWrapper) => { + const children = getPageHeader(wrapper).children || null; + + return shallow(
{children}
); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts index ed5c3f85a888e..7903b4a31c8a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts @@ -15,6 +15,7 @@ export { getPageTitle, getPageDescription, getPageHeaderActions, + getPageHeaderChildren, } from './get_page_header'; // Misc From cc051ceda521f419004abaf6fa07c3ae540623f2 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Tue, 22 Jun 2021 19:48:28 +0200 Subject: [PATCH 063/118] [CCR] Migrate to new page layout structure (#102507) (#102946) * wip: start migrating views from ccr * finish up migrating ccr pages to new nav layout * Fix tests, linter errors and i18n strings * remove todo * Render loading and error states centered in screen without page title * Keep loader going while we still setting the payload Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../auto_follow_pattern_add.test.js | 8 +- .../follower_index_add.test.js | 8 +- .../public/app/app.tsx | 62 ++---- .../auto_follow_pattern_page_title.js | 61 ++---- .../components/follower_index_page_title.js | 61 ++---- .../auto_follow_pattern_add.js | 56 ++--- .../auto_follow_pattern_edit.js | 152 ++++++------- .../follower_index_add/follower_index_add.js | 55 ++--- .../follower_index_edit.js | 162 +++++++------- .../auto_follow_pattern_list.js | 204 +++++++---------- .../auto_follow_pattern_table.js | 17 ++ .../components/context_menu/context_menu.js | 2 +- .../follower_indices_table.js | 16 ++ .../follower_indices_list.js | 205 +++++++----------- .../public/app/sections/home/home.js | 51 ++--- .../public/shared_imports.ts | 7 +- 16 files changed, 513 insertions(+), 614 deletions(-) diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js index 86abbba968781..e49751cecc1d0 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -39,10 +39,6 @@ describe('Create Auto-follow pattern', () => { expect(exists('remoteClustersLoading')).toBe(true); expect(find('remoteClustersLoading').text()).toBe('Loading remote clusters…'); }); - - test('should have a link to the documentation', () => { - expect(exists('docsButton')).toBe(true); - }); }); describe('when remote clusters are loaded', () => { @@ -59,6 +55,10 @@ describe('Create Auto-follow pattern', () => { component.update(); }); + test('should have a link to the documentation', () => { + expect(exists('docsButton')).toBe(true); + }); + test('should display the Auto-follow pattern form', async () => { expect(exists('autoFollowPatternForm')).toBe(true); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js index 228868194b231..6d54444df4273 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js @@ -42,10 +42,6 @@ describe('Create Follower index', () => { expect(exists('remoteClustersLoading')).toBe(true); expect(find('remoteClustersLoading').text()).toBe('Loading remote clusters…'); }); - - test('should have a link to the documentation', () => { - expect(exists('docsButton')).toBe(true); - }); }); describe('when remote clusters are loaded', () => { @@ -62,6 +58,10 @@ describe('Create Follower index', () => { component.update(); }); + test('should have a link to the documentation', () => { + expect(exists('docsButton')).toBe(true); + }); + test('should display the Follower index form', async () => { expect(exists('followerIndexForm')).toBe(true); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx index 50a6cfb1b4bb9..c6144143e1849 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx +++ b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx @@ -5,27 +5,19 @@ * 2.0. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import { Route, Switch, Router, Redirect } from 'react-router-dom'; import { ScopedHistory, ApplicationStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageContent, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; +import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui'; import { getFatalErrors } from './services/notifications'; -import { SectionError } from './components'; import { routing } from './services/routing'; // @ts-ignore import { loadPermissions } from './services/api'; +import { SectionLoading, PageError } from '../shared_imports'; // @ts-ignore import { @@ -119,48 +111,34 @@ class AppComponent extends Component { if (isFetchingPermissions) { return ( - - - - - - - - -

- -

-
-
-
+ + + + ); } if (fetchPermissionError) { return ( - - - } - error={fetchPermissionError} - /> - - - + + } + error={fetchPermissionError} + /> ); } if (!hasPermission) { return ( - + ( - - + <> + {title}} + rightSideItems={[ + + + , + ]} + /> - - - - -

{title}

-
-
- - - - - - -
-
-
+ + ); AutoFollowPatternPageTitle.propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js index b5652d3f2b6e6..6d523cf2c470f 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js @@ -5,51 +5,38 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiPageContentHeader, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; +import { EuiSpacer, EuiPageHeader, EuiButtonEmpty } from '@elastic/eui'; import { documentationLinks } from '../services/documentation_links'; export const FollowerIndexPageTitle = ({ title }) => ( - - + <> + {title}} + rightSideItems={[ + + + , + ]} + /> - - - - -

{title}

-
-
- - - - - - -
-
-
+ + ); FollowerIndexPageTitle.propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js index 0fe562b7a8f05..118e3103008d0 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js @@ -8,16 +8,15 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContentBody, EuiPageContent } from '@elastic/eui'; import { listBreadcrumb, addBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; import { AutoFollowPatternForm, AutoFollowPatternPageTitle, RemoteClustersProvider, - SectionLoading, } from '../../components'; +import { SectionLoading } from '../../../shared_imports'; export class AutoFollowPatternAdd extends PureComponent { static propTypes = { @@ -44,30 +43,37 @@ export class AutoFollowPatternAdd extends PureComponent { } = this.props; return ( - - - } - /> - - - {({ isLoading, error, remoteClusters }) => { - if (isLoading) { - return ( - + + {({ isLoading, error, remoteClusters }) => { + if (isLoading) { + return ( + + - ); - } + + ); + } + + return ( + + + } + /> - return ( } /> - ); - }} - - +
+ ); + }} + ); } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js index d060fb83832c6..fa97b28c8b472 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js @@ -5,12 +5,12 @@ * 2.0. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiPageContent, EuiEmptyPrompt, EuiPageContentBody } from '@elastic/eui'; import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; @@ -18,10 +18,9 @@ import { AutoFollowPatternForm, AutoFollowPatternPageTitle, RemoteClustersProvider, - SectionLoading, - SectionError, } from '../../components'; import { API_STATUS } from '../../constants'; +import { SectionLoading } from '../../../shared_imports'; export class AutoFollowPatternEdit extends PureComponent { static propTypes = { @@ -80,13 +79,6 @@ export class AutoFollowPatternEdit extends PureComponent { }, } = this.props; - const title = i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorTitle', - { - defaultMessage: 'Error loading auto-follow pattern', - } - ); - const errorMessage = error.body.statusCode === 404 ? { @@ -101,38 +93,42 @@ export class AutoFollowPatternEdit extends PureComponent { : error; return ( - - - - - - - - + + + + } + body={

{errorMessage}

} + actions={ + -
-
-
-
+ + } + /> + ); } - renderLoadingAutoFollowPattern() { + renderLoading(loadingTitle) { return ( - - - + + {loadingTitle} + ); } @@ -145,55 +141,59 @@ export class AutoFollowPatternEdit extends PureComponent { match: { url: currentUrl }, } = this.props; + if (apiStatus.get === API_STATUS.LOADING || !autoFollowPattern) { + return this.renderLoading( + i18n.translate('xpack.crossClusterReplication.autoFollowPatternEditForm.loadingTitle', { + defaultMessage: 'Loading auto-follow pattern…', + }) + ); + } + + if (apiError.get) { + return this.renderGetAutoFollowPatternError(apiError.get); + } + return ( - - + + {({ isLoading, error, remoteClusters }) => { + if (isLoading) { + return this.renderLoading( + i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingRemoteClustersMessage', + { defaultMessage: 'Loading remote clusters…' } + ) + ); } - /> - {apiStatus.get === API_STATUS.LOADING && this.renderLoadingAutoFollowPattern()} - - {apiError.get && this.renderGetAutoFollowPatternError(apiError.get)} - - {autoFollowPattern && ( - - {({ isLoading, error, remoteClusters }) => { - if (isLoading) { - return ( - - - - ); - } + return ( + + + } + /> - return ( - - } - /> - ); - }} - - )} - + + } + /> + + ); + }} + ); } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js index 836a4f5cc36fa..325c23641580c 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js @@ -8,16 +8,15 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContentBody, EuiPageContent } from '@elastic/eui'; import { setBreadcrumbs, listBreadcrumb, addBreadcrumb } from '../../services/breadcrumbs'; import { FollowerIndexForm, FollowerIndexPageTitle, RemoteClustersProvider, - SectionLoading, } from '../../components'; +import { SectionLoading } from '../../../shared_imports'; export class FollowerIndexAdd extends PureComponent { static propTypes = { @@ -45,30 +44,36 @@ export class FollowerIndexAdd extends PureComponent { } = this.props; return ( - - - } - /> - - - {({ isLoading, error, remoteClusters }) => { - if (isLoading) { - return ( - + + {({ isLoading, error, remoteClusters }) => { + if (isLoading) { + return ( + + - ); - } + + ); + } - return ( + return ( + + + } + /> } /> - ); - }} - - + + ); + }} + ); } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js index 41b09a398b1f2..618d97f186516 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js @@ -5,18 +5,17 @@ * 2.0. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiButtonEmpty, + EuiButton, EuiConfirmModal, - EuiFlexGroup, - EuiFlexItem, + EuiPageContentBody, EuiPageContent, - EuiSpacer, + EuiEmptyPrompt, } from '@elastic/eui'; import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs'; @@ -24,11 +23,10 @@ import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_rea import { FollowerIndexForm, FollowerIndexPageTitle, - SectionLoading, - SectionError, RemoteClustersProvider, } from '../../components'; import { API_STATUS } from '../../constants'; +import { SectionLoading } from '../../../shared_imports'; export class FollowerIndexEdit extends PureComponent { static propTypes = { @@ -104,14 +102,11 @@ export class FollowerIndexEdit extends PureComponent { closeConfirmModal = () => this.setState({ showConfirmModal: false }); - renderLoadingFollowerIndex() { + renderLoading(loadingTitle) { return ( - - - + + {loadingTitle} + ); } @@ -122,13 +117,6 @@ export class FollowerIndexEdit extends PureComponent { }, } = this.props; - const title = i18n.translate( - 'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorTitle', - { - defaultMessage: 'Error loading follower index', - } - ); - const errorMessage = error.body.statusCode === 404 ? { @@ -143,27 +131,33 @@ export class FollowerIndexEdit extends PureComponent { : error; return ( - - - - - - - - + + + + } + body={

{errorMessage}

} + actions={ + -
-
-
-
+ + } + /> + ); } @@ -237,57 +231,63 @@ export class FollowerIndexEdit extends PureComponent { /* remove non-editable properties */ const { shards, ...rest } = followerIndex || {}; // eslint-disable-line no-unused-vars + if (apiStatus.get === API_STATUS.LOADING || !followerIndex) { + return this.renderLoading( + i18n.translate( + 'xpack.crossClusterReplication.followerIndexEditForm.loadingFollowerIndexTitle', + { defaultMessage: 'Loading follower index…' } + ) + ); + } + + if (apiError.get) { + return this.renderGetFollowerIndexError(apiError.get); + } + return ( - - + + {({ isLoading, error, remoteClusters }) => { + if (isLoading) { + return this.renderLoading( + i18n.translate( + 'xpack.crossClusterReplication.followerIndexEditForm.loadingRemoteClustersMessage', + { defaultMessage: 'Loading remote clusters…' } + ) + ); } - /> - {apiStatus.get === API_STATUS.LOADING && this.renderLoadingFollowerIndex()} - - {apiError.get && this.renderGetFollowerIndexError(apiError.get)} - {followerIndex && ( - - {({ isLoading, error, remoteClusters }) => { - if (isLoading) { - return ( - - - - ); - } + return ( + + + } + /> - return ( - - } - /> - ); - }} - - )} + + } + /> - {showConfirmModal && this.renderConfirmModal()} - + {showConfirmModal && this.renderConfirmModal()} + + ); + }} + ); } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js index 1885f33f9d633..1ab4e1a3e003a 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js @@ -5,24 +5,17 @@ * 2.0. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButton, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiText, EuiSpacer } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; -import { extractQueryParams } from '../../../../shared_imports'; +import { extractQueryParams, SectionLoading } from '../../../../shared_imports'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants'; -import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; +import { SectionError, SectionUnauthorized } from '../../../components'; import { AutoFollowPatternTable, DetailPanel } from './components'; const REFRESH_RATE_MS = 30000; @@ -103,47 +96,77 @@ export class AutoFollowPatternList extends PureComponent { clearInterval(this.interval); } - renderHeader() { - const { isAuthorized, history } = this.props; + renderEmpty() { + return ( +
+ + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> +
+ ); + } + + renderList() { + const { selectAutoFollowPattern, autoFollowPatterns } = this.props; + const { isDetailPanelOpen } = this.state; + return ( - - - - -

- -

-
-
+ <> + +

+ +

+
+ + - - {isAuthorized && ( - - - - )} - -
+ - -
+ {isDetailPanelOpen && ( + selectAutoFollowPattern(null)} /> + )} + ); } - renderContent(isEmpty) { - const { apiError, apiStatus, isAuthorized } = this.props; + render() { + const { autoFollowPatterns, apiError, apiStatus, isAuthorized } = this.props; + const isEmpty = apiStatus === API_STATUS.IDLE && !autoFollowPatterns.length; if (!isAuthorized) { return ( @@ -171,12 +194,7 @@ export class AutoFollowPatternList extends PureComponent { } ); - return ( - - - - - ); + return ; } if (isEmpty) { @@ -185,83 +203,17 @@ export class AutoFollowPatternList extends PureComponent { if (apiStatus === API_STATUS.LOADING) { return ( - - - +
+ + + +
); } return this.renderList(); } - - renderEmpty() { - return ( - - - - } - body={ - -

- -

-
- } - actions={ - - - - } - data-test-subj="emptyPrompt" - /> - ); - } - - renderList() { - const { selectAutoFollowPattern, autoFollowPatterns } = this.props; - - const { isDetailPanelOpen } = this.state; - - return ( - <> - - {isDetailPanelOpen && ( - selectAutoFollowPattern(null)} /> - )} - - ); - } - - render() { - const { autoFollowPatterns, apiStatus } = this.props; - const isEmpty = apiStatus === API_STATUS.IDLE && !autoFollowPatterns.length; - - return ( - - {!isEmpty && this.renderHeader()} - {this.renderContent(isEmpty)} - - ); - } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 87002c936179a..0d228f2e63802 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -8,13 +8,17 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + import { EuiInMemoryTable, + EuiButton, EuiLink, EuiLoadingKibana, EuiOverlayMask, EuiHealth, } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK } from '../../../../../constants'; import { AutoFollowPatternDeleteProvider, @@ -305,6 +309,19 @@ export class AutoFollowPatternTable extends PureComponent { )} /> ) : undefined, + toolsRight: ( + + + + ), onChange: this.onSearch, box: { incremental: true, diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js index 0d0943d870266..866afa3e6e6dc 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js @@ -97,7 +97,7 @@ export class ContextMenu extends PureComponent { anchorPosition={anchorPosition} repositionOnScroll > - + ) : undefined, + toolsRight: ( + + + + ), onChange: this.onSearch, box: { incremental: true, diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index 743a9ec47e689..a52ba0e613ca9 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -5,24 +5,17 @@ * 2.0. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButton, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiText, EuiSpacer } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; -import { extractQueryParams } from '../../../../shared_imports'; +import { extractQueryParams, SectionLoading } from '../../../../shared_imports'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; -import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; +import { SectionError, SectionUnauthorized } from '../../../components'; import { FollowerIndicesTable, DetailPanel } from './components'; const REFRESH_RATE_MS = 30000; @@ -94,47 +87,87 @@ export class FollowerIndicesList extends PureComponent { clearInterval(this.interval); } - renderHeader() { - const { isAuthorized, history } = this.props; + renderEmpty() { + return ( +
+ + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> +
+ ); + } + renderLoading() { return ( - - - - -

- -

-
-
+
+ + + +
+ ); + } + + renderList() { + const { selectFollowerIndex, followerIndices } = this.props; + + const { isDetailPanelOpen } = this.state; + + return ( + <> + +

+ +

+
- - {isAuthorized && ( - - - - )} - -
+ - -
+ + + {isDetailPanelOpen && selectFollowerIndex(null)} />} + ); } - renderContent(isEmpty) { - const { apiError, isAuthorized, apiStatus } = this.props; + render() { + const { followerIndices, apiError, isAuthorized, apiStatus } = this.props; + const isEmpty = apiStatus === API_STATUS.IDLE && !followerIndices.length; if (!isAuthorized) { return ( @@ -162,12 +195,7 @@ export class FollowerIndicesList extends PureComponent { } ); - return ( - - - - - ); + return ; } if (isEmpty) { @@ -180,79 +208,4 @@ export class FollowerIndicesList extends PureComponent { return this.renderList(); } - - renderEmpty() { - return ( - - - - } - body={ - -

- -

-
- } - actions={ - - - - } - data-test-subj="emptyPrompt" - /> - ); - } - - renderLoading() { - return ( - - - - ); - } - - renderList() { - const { selectFollowerIndex, followerIndices } = this.props; - - const { isDetailPanelOpen } = this.state; - - return ( - - - {isDetailPanelOpen && selectFollowerIndex(null)} />} - - ); - } - - render() { - const { followerIndices, apiStatus } = this.props; - const isEmpty = apiStatus === API_STATUS.IDLE && !followerIndices.length; - return ( - - {!isEmpty && this.renderHeader()} - {this.renderContent(isEmpty)} - - ); - } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js index ff37c2157d515..70d35dcb22569 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js @@ -9,7 +9,7 @@ import React, { PureComponent } from 'react'; import { Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; +import { EuiSpacer, EuiPageHeader } from '@elastic/eui'; import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs'; import { routing } from '../../services/routing'; @@ -66,40 +66,33 @@ export class CrossClusterReplicationHome extends PureComponent { render() { return ( - - - -

+ <> + -

-
- - - - - {this.tabs.map((tab) => ( - this.onSectionChange(tab.id)} - isSelected={tab.id === this.state.activeSection} - key={tab.id} - data-test-subj={tab.testSubj} - > - {tab.name} - - ))} - + + } + tabs={this.tabs.map((tab) => ({ + onClick: () => this.onSectionChange(tab.id), + isSelected: tab.id === this.state.activeSection, + key: tab.id, + 'data-test-subj': tab.testSubj, + label: tab.name, + }))} + /> - + - - - - -
-
+ + + + + ); } } diff --git a/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts index fd28175318666..55a10749230c7 100644 --- a/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts +++ b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts @@ -5,4 +5,9 @@ * 2.0. */ -export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public'; +export { + extractQueryParams, + indices, + SectionLoading, + PageError, +} from '../../../../src/plugins/es_ui_shared/public'; From 8e2c6541ff04051cd02a3f57f589a7ca904e8146 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 22 Jun 2021 19:50:33 +0200 Subject: [PATCH 064/118] [AppService] fix deepLinks being lost when updating the app with other fields (#102895) (#102947) * fix app updater for deepLinks * improve implem --- .../application/application_service.test.ts | 83 ++++++++++++++++++- .../application/application_service.tsx | 7 +- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 3ed164088bf5c..de9e4d4496f3b 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -15,13 +15,13 @@ import { import { createElement } from 'react'; import { BehaviorSubject, Subject } from 'rxjs'; import { bufferCount, take, takeUntil } from 'rxjs/operators'; -import { shallow, mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { httpServiceMock } from '../http/http_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; -import { App, PublicAppInfo, AppNavLinkStatus, AppStatus, AppUpdater } from './types'; +import { App, AppDeepLink, AppNavLinkStatus, AppStatus, AppUpdater, PublicAppInfo } from './types'; import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { @@ -365,6 +365,85 @@ describe('#setup()', () => { expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); MockHistory.push.mockClear(); }); + + it('preserves the deep links if the update does not modify them', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + const updater$ = new BehaviorSubject((app) => ({})); + + const deepLinks: AppDeepLink[] = [ + { + id: 'foo', + title: 'Foo', + searchable: true, + navLinkStatus: AppNavLinkStatus.visible, + path: '/foo', + }, + { + id: 'bar', + title: 'Bar', + searchable: false, + navLinkStatus: AppNavLinkStatus.hidden, + path: '/bar', + }, + ]; + + setup.register(pluginId, createApp({ id: 'app1', deepLinks, updater$ })); + + const { applications$ } = await service.start(startDeps); + + updater$.next((app) => ({ defaultPath: '/foo' })); + + let appInfos = await applications$.pipe(take(1)).toPromise(); + + expect(appInfos.get('app1')!.deepLinks).toEqual([ + { + deepLinks: [], + id: 'foo', + keywords: [], + navLinkStatus: 1, + path: '/foo', + searchable: true, + title: 'Foo', + }, + { + deepLinks: [], + id: 'bar', + keywords: [], + navLinkStatus: 3, + path: '/bar', + searchable: false, + title: 'Bar', + }, + ]); + + updater$.next((app) => ({ + deepLinks: [ + { + id: 'bar', + title: 'Bar', + searchable: false, + navLinkStatus: AppNavLinkStatus.hidden, + path: '/bar', + }, + ], + })); + + appInfos = await applications$.pipe(take(1)).toPromise(); + + expect(appInfos.get('app1')!.deepLinks).toEqual([ + { + deepLinks: [], + id: 'bar', + keywords: [], + navLinkStatus: 3, + path: '/bar', + searchable: false, + title: 'Bar', + }, + ]); + }); }); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 8c6090caabce1..2e804bf2f5413 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -54,6 +54,7 @@ function filterAvailable(m: Map, capabilities: Capabilities) { ) ); } + const findMounter = (mounters: Map, appRoute?: string) => [...mounters].find(([, mounter]) => mounter.appRoute === appRoute); @@ -414,13 +415,11 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { changes.navLinkStatus ?? AppNavLinkStatus.default, fields.navLinkStatus ?? AppNavLinkStatus.default ), - // deepLinks take the last defined update - deepLinks: fields.deepLinks - ? populateDeepLinkDefaults(fields.deepLinks) - : changes.deepLinks, + ...(fields.deepLinks ? { deepLinks: populateDeepLinkDefaults(fields.deepLinks) } : {}), }; } }); + return { ...app, ...changes, From ba736fc3876380f5864ee67d7077c23b12e0215b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:08:07 -0400 Subject: [PATCH 065/118] [App Search] Convert Search UI view to new page template + minor UI polish (#102813) (#102952) * Convert Search UI view to use new page template + update tests TODO * [UX polish] Add empty state to Search UI view - On a totally new engine, all pages except this one* had an empty state, so per Davey's recommendations I whipped up a new empty state for this page * Overview has a custom 'empty' state, analytics does not have an empty state * Update router * Fix bad merge conflict resolution * [Polish] Copy feedback proposed by Davey - see https://github.com/elastic/kibana/pull/101958/commits/cbc3706223eb47be3d854a1cf4e3c7275d88ca39 Co-authored-by: Constance --- .../components/engine/engine_router.tsx | 10 +- .../search_ui/components/empty_state.test.tsx | 27 ++++ .../search_ui/components/empty_state.tsx | 46 +++++++ .../components/search_ui/search_ui.test.tsx | 13 +- .../components/search_ui/search_ui.tsx | 120 ++++++++---------- 5 files changed, 143 insertions(+), 73 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx 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 b390b1a52b927..3e18c9e680de2 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 @@ -109,6 +109,11 @@ export const EngineRouter: React.FC = () => { )} + {canManageEngineSearchUi && ( + + + + )} {/* TODO: Remove layout once page template migration is over */} }> {canViewEngineSchema && ( @@ -141,11 +146,6 @@ export const EngineRouter: React.FC = () => { )} - {canManageEngineSearchUi && ( - - - - )} {canViewMetaEngineSourceEngines && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx new file mode 100644 index 0000000000000..39f0cb376b325 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './empty_state'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Add documents to generate a Search UI'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/reference-ui-guide.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx new file mode 100644 index 0000000000000..b7665a58de300 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState: React.FC = () => ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.title', { + defaultMessage: 'Add documents to generate a Search UI', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.description', { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', + })} +

+ } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.buttonLabel', { + defaultMessage: 'Read the Search UI guide', + })} + + } + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx index edec376dd3edd..f9f0dd611b953 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx @@ -6,14 +6,17 @@ */ import '../../../__mocks__/shallow_useeffect.mock'; -import '../../__mocks__/engine_logic.mock'; -import { setMockActions } from '../../../__mocks__/kea_logic'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; +import { mockEngineValues } from '../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; +import { SearchUIForm } from './components/search_ui_form'; +import { SearchUIGraphic } from './components/search_ui_graphic'; + import { SearchUI } from './'; describe('SearchUI', () => { @@ -24,11 +27,13 @@ describe('SearchUI', () => { beforeEach(() => { jest.clearAllMocks(); setMockActions(actions); + setMockValues(mockEngineValues); }); it('renders', () => { - shallow(); - // TODO: Check for form + const wrapper = shallow(); + expect(wrapper.find(SearchUIForm).exists()).toBe(true); + expect(wrapper.find(SearchUIGraphic).exists()).toBe(true); }); it('initializes data on mount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx index e75bc36177151..0ac59a33068ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx @@ -7,25 +7,16 @@ import React, { useEffect } from 'react'; -import { useActions } from 'kea'; +import { useActions, useValues } from 'kea'; -import { - EuiPageHeader, - EuiPageContentBody, - EuiText, - EuiFlexItem, - EuiFlexGroup, - EuiSpacer, - EuiLink, -} from '@elastic/eui'; +import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - import { DOCS_PREFIX } from '../../routes'; -import { getEngineBreadcrumbs } from '../engine'; +import { EngineLogic, getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; +import { EmptyState } from './components/empty_state'; import { SearchUIForm } from './components/search_ui_form'; import { SearchUIGraphic } from './components/search_ui_graphic'; import { SEARCH_UI_TITLE } from './i18n'; @@ -33,61 +24,62 @@ import { SearchUILogic } from './search_ui_logic'; export const SearchUI: React.FC = () => { const { loadFieldData } = useActions(SearchUILogic); + const { engine } = useValues(EngineLogic); useEffect(() => { loadFieldData(); }, []); return ( - <> - - - - - - - -

- - - - ), - }} - /> -

-

- - - - ), - }} - /> -

-
- - -
- - - -
-
- + } + > + + + +

+ + + + ), + }} + /> +

+

+ + + + ), + }} + /> +

+
+ + +
+ + + +
+
); }; From 11dec592014598e83d35988c4ef79f720639eacd Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:30:31 -0400 Subject: [PATCH 066/118] [Discover][Main] Improve state related code (#102028) (#102959) Co-authored-by: Matthias Wilhelm --- .../layout/discover_layout.test.tsx | 4 +- .../components/layout/discover_layout.tsx | 91 ++------- .../apps/main/components/layout/types.ts | 8 +- .../components/sidebar/discover_sidebar.tsx | 22 +- .../discover_sidebar_responsive.test.tsx | 11 - .../sidebar/discover_sidebar_responsive.tsx | 9 - .../sidebar/lib/group_fields.test.ts | 6 +- .../components/sidebar/lib/group_fields.tsx | 4 +- .../apps/main/discover_main_app.tsx | 46 +---- .../apps/main/services/discover_state.ts | 68 ++++++- .../main/services/use_discover_state.test.ts | 4 - .../apps/main/services/use_discover_state.ts | 188 ++++++++++++------ .../main/services/use_saved_search.test.ts | 52 ++++- .../apps/main/services/use_saved_search.ts | 135 +++++-------- 14 files changed, 323 insertions(+), 325 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx index 2fd394d98281b..57a9d518f838e 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx @@ -18,7 +18,6 @@ import { createSearchSourceMock } from '../../../../../../../data/common/search/ import { IndexPattern, IndexPatternAttributes } from '../../../../../../../data/common'; import { SavedObject } from '../../../../../../../../core/types'; import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield'; -import { DiscoverSearchSessionManager } from '../../services/discover_search_session'; import { GetStateReturn } from '../../services/discover_state'; import { DiscoverLayoutProps } from './types'; import { SavedSearchDataSubject } from '../../services/use_saved_search'; @@ -50,11 +49,12 @@ function getProps(indexPattern: IndexPattern): DiscoverLayoutProps { indexPattern, indexPatternList, navigateTo: jest.fn(), + onChangeIndexPattern: jest.fn(), + onUpdateQuery: jest.fn(), resetQuery: jest.fn(), savedSearch: savedSearchMock, savedSearchData$: savedSearch$, savedSearchRefetch$: new Subject(), - searchSessionManager: {} as DiscoverSearchSessionManager, searchSource: searchSourceMock, services, state: { columns: [] }, diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 0430614d413b6..a10674323e5cb 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -36,10 +36,8 @@ import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, - MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, - SORT_DEFAULT_ORDER_SETTING, } from '../../../../../../common'; import { popularizeField } from '../../../../helpers/popularize_field'; import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; @@ -52,7 +50,6 @@ import { InspectorSession } from '../../../../../../../inspector/public'; import { DiscoverUninitialized } from '../uninitialized/uninitialized'; import { SavedSearchDataMessage } from '../../services/use_saved_search'; import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns'; -import { getSwitchIndexPatternAppState } from '../../utils/get_switch_index_pattern_app_state'; import { FetchStatus } from '../../../../types'; const DocTableLegacyMemoized = React.memo(DocTableLegacy); @@ -72,26 +69,20 @@ export function DiscoverLayout({ indexPattern, indexPatternList, navigateTo, + onChangeIndexPattern, + onUpdateQuery, savedSearchRefetch$, resetQuery, savedSearchData$, savedSearch, - searchSessionManager, searchSource, services, state, stateContainer, }: DiscoverLayoutProps) { - const { - trackUiMetric, - capabilities, - indexPatterns, - data, - uiSettings: config, - filterManager, - } = services; + const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services; - const sampleSize = useMemo(() => config.get(SAMPLE_SIZE_SETTING), [config]); + const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]); const [expandedDoc, setExpandedDoc] = useState(undefined); const [inspectorSession, setInspectorSession] = useState(undefined); const scrollableDesktop = useRef(null); @@ -121,42 +112,21 @@ export function DiscoverLayout({ }; }, [savedSearchData$, fetchState]); - const isMobile = () => { - // collapse icon isn't displayed in mobile view, use it to detect which view is displayed - return collapseIcon && !collapseIcon.current; - }; + // collapse icon isn't displayed in mobile view, use it to detect which view is displayed + const isMobile = () => collapseIcon && !collapseIcon.current; const timeField = useMemo(() => { return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; }, [indexPattern]); const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const isLegacy = useMemo(() => services.uiSettings.get(DOC_TABLE_LEGACY), [services]); - const useNewFieldsApi = useMemo(() => !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [ - services, - ]); - - const unmappedFieldsConfig = useMemo( - () => ({ - showUnmappedFields: useNewFieldsApi, - }), - [useNewFieldsApi] - ); + const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); + const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); const resultState = useMemo(() => getResultState(fetchStatus, rows!), [fetchStatus, rows]); - const updateQuery = useCallback( - (_payload, isUpdate?: boolean) => { - if (isUpdate === false) { - searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); - savedSearchRefetch$.next(); - } - }, - [savedSearchRefetch$, searchSessionManager] - ); - const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({ capabilities, - config, + config: uiSettings, indexPattern, indexPatterns, setAppState: stateContainer.setAppState, @@ -243,42 +213,8 @@ export function DiscoverLayout({ const contentCentered = resultState === 'uninitialized'; const showTimeCol = useMemo( - () => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, - [config, indexPattern.timeFieldName] - ); - - const onChangeIndexPattern = useCallback( - async (id: string) => { - const nextIndexPattern = await indexPatterns.get(id); - if (nextIndexPattern && indexPattern) { - /** - * Without resetting the fetch state, e.g. a time column would be displayed when switching - * from a index pattern without to a index pattern with time filter for a brief moment - * That's because appState is updated before savedSearchData$ - * The following line of code catches this, but should be improved - */ - savedSearchData$.next({ rows: [], state: FetchStatus.LOADING, fieldCounts: {} }); - - const nextAppState = getSwitchIndexPatternAppState( - indexPattern, - nextIndexPattern, - state.columns || [], - (state.sort || []) as SortPairArr[], - config.get(MODIFY_COLUMNS_ON_SWITCH), - config.get(SORT_DEFAULT_ORDER_SETTING) - ); - stateContainer.setAppState(nextAppState); - } - }, - [ - config, - indexPattern, - indexPatterns, - savedSearchData$, - state.columns, - state.sort, - stateContainer, - ] + () => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + [uiSettings, indexPattern.timeFieldName] ); return ( @@ -294,7 +230,7 @@ export function DiscoverLayout({ searchSource={searchSource} services={services} stateContainer={stateContainer} - updateQuery={updateQuery} + updateQuery={onUpdateQuery} />

@@ -316,7 +252,6 @@ export function DiscoverLayout({ state={state} isClosed={isSidebarClosed} trackUiMetric={trackUiMetric} - unmappedFieldsConfig={unmappedFieldsConfig} useNewFieldsApi={useNewFieldsApi} onEditRuntimeField={onEditRuntimeField} /> @@ -373,7 +308,7 @@ export function DiscoverLayout({ > >; - resetQuery: () => void; navigateTo: (url: string) => void; + onChangeIndexPattern: (id: string) => void; + onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + resetQuery: () => void; savedSearch: SavedSearch; savedSearchData$: SavedSearchDataSubject; savedSearchRefetch$: SavedSearchRefetchSubject; - searchSessionManager: DiscoverSearchSessionManager; searchSource: ISearchSource; services: DiscoverServices; state: AppState; diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx index 7fbbf6fd3ffdc..7f8866a2ee369 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx @@ -82,7 +82,6 @@ export function DiscoverSidebar({ trackUiMetric, useNewFieldsApi = false, useFlyout = false, - unmappedFieldsConfig, onEditRuntimeField, onChangeIndexPattern, setFieldEditorRef, @@ -129,25 +128,8 @@ export function DiscoverSidebar({ popular: popularFields, unpopular: unpopularFields, } = useMemo( - () => - groupFields( - fields, - columns, - popularLimit, - fieldCounts, - fieldFilter, - useNewFieldsApi, - !!unmappedFieldsConfig?.showUnmappedFields - ), - [ - fields, - columns, - popularLimit, - fieldCounts, - fieldFilter, - useNewFieldsApi, - unmappedFieldsConfig?.showUnmappedFields, - ] + () => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi), + [fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi] ); const paginate = useCallback(() => { diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx index 2ad75806173eb..6973221fd3624 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -25,7 +25,6 @@ import { } from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../../../build_services'; import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; -import { DiscoverSidebar } from './discover_sidebar'; const mockServices = ({ history: () => ({ @@ -132,14 +131,4 @@ describe('discover responsive sidebar', function () { findTestSubject(comp, 'plus-extension-gif').simulate('click'); expect(props.onAddFilter).toHaveBeenCalled(); }); - it('renders sidebar with unmapped fields config', function () { - const unmappedFieldsConfig = { - showUnmappedFields: false, - }; - const componentProps = { ...props, unmappedFieldsConfig }; - const component = mountWithIntl(); - const discoverSidebar = component.find(DiscoverSidebar); - expect(discoverSidebar).toHaveLength(1); - expect(discoverSidebar.props().unmappedFieldsConfig).toEqual(unmappedFieldsConfig); - }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx index cc33601f77728..003bb22599e48 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx @@ -105,15 +105,6 @@ export interface DiscoverSidebarResponsiveProps { * Read from the Fields API */ useNewFieldsApi?: boolean; - /** - * an object containing properties for proper handling of unmapped fields - */ - unmappedFieldsConfig?: { - /** - * determines whether to display unmapped fields - */ - showUnmappedFields: boolean; - }; /** * callback to execute on edit runtime field */ diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts index 5869720635621..cd9f6b3cac4a5 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts @@ -244,8 +244,7 @@ describe('group_fields', function () { 5, fieldCounts, fieldFilterState, - true, - false + true ); expect(actual.unpopular).toEqual([]); }); @@ -270,8 +269,7 @@ describe('group_fields', function () { 5, fieldCounts, fieldFilterState, - false, - undefined + false ); expect(actual.unpopular.map((field) => field.name)).toEqual(['unknown_field']); }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx index dc6cbcedc8086..2007d32fe84be 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx @@ -24,9 +24,9 @@ export function groupFields( popularLimit: number, fieldCounts: Record, fieldFilterState: FieldFilterState, - useNewFieldsApi: boolean, - showUnmappedFields = true + useNewFieldsApi: boolean ): GroupedFields { + const showUnmappedFields = useNewFieldsApi; const result: GroupedFields = { selected: [], popular: [], diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx index 5cc7147b49ff9..07939fff6e7f4 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx @@ -5,15 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useMemo, useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { History } from 'history'; import { DiscoverLayout } from './components/layout'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; -import { useSavedSearch as useSavedSearchData } from './services/use_saved_search'; import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; import { useDiscoverState } from './services/use_discover_state'; -import { useSearchSession } from './services/use_search_session'; import { useUrl } from './services/use_url'; import { IndexPattern, IndexPatternAttributes, SavedObject } from '../../../../../data/common'; import { DiscoverServices } from '../../../build_services'; @@ -55,18 +52,20 @@ export function DiscoverMainApp(props: DiscoverMainProps) { const { services, history, navigateTo, indexPatternList } = props.opts; const { chrome, docLinks, uiSettings: config, data } = services; - const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); - /** * State related logic */ const { - stateContainer, - state, + data$, indexPattern, - searchSource, - savedSearch, + onChangeIndexPattern, + onUpdateQuery, + refetch$, resetSavedSearch, + savedSearch, + searchSource, + state, + stateContainer, } = useDiscoverState({ services, history, @@ -79,25 +78,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) { */ useUrl({ history, resetSavedSearch }); - /** - * Search session logic - */ - const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch }); - - /** - * Data fetching logic - */ - const { data$, refetch$ } = useSavedSearchData({ - indexPattern, - savedSearch, - searchSessionManager, - searchSource, - services, - state, - stateContainer, - useNewFieldsApi, - }); - /** * SavedSearch depended initializing */ @@ -115,11 +95,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) { */ useEffect(() => { addHelpMenuToAppChrome(chrome, docLinks); - stateContainer.replaceUrlAppState({}).then(() => { - stateContainer.startSync(); - }); - - return () => stateContainer.stopSync(); }, [stateContainer, chrome, docLinks]); const resetQuery = useCallback(() => { @@ -130,12 +105,13 @@ export function DiscoverMainApp(props: DiscoverMainProps) { ; + /** + * Function starting state sync when Discover main is loaded + */ + initializeAndSync: ( + indexPattern: IndexPattern, + filterManager: FilterManager, + data: DataPublicPluginStart + ) => () => void; /** * Start sync between state and URL */ @@ -204,16 +216,18 @@ export function getState({ stateStorage, }); + const replaceUrlAppState = async (newPartial: AppState = {}) => { + const state = { ...appStateContainer.getState(), ...newPartial }; + await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true }); + }; + return { kbnUrlStateStorage: stateStorage, appStateContainer: appStateContainerModified, startSync: start, stopSync: stop, setAppState: (newPartial: AppState) => setState(appStateContainerModified, newPartial), - replaceUrlAppState: async (newPartial: AppState = {}) => { - const state = { ...appStateContainer.getState(), ...newPartial }; - await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true }); - }, + replaceUrlAppState, resetInitialAppState: () => { initialAppState = appStateContainer.getState(); }, @@ -224,6 +238,50 @@ export function getState({ getPreviousAppState: () => previousAppState, flushToUrl: () => stateStorage.kbnUrlControls.flush(), isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()), + initializeAndSync: ( + indexPattern: IndexPattern, + filterManager: FilterManager, + data: DataPublicPluginStart + ) => { + if (appStateContainer.getState().index !== indexPattern.id) { + // used index pattern is different than the given by url/state which is invalid + setState(appStateContainerModified, { index: indexPattern.id }); + } + // sync initial app filters from state to filterManager + const filters = appStateContainer.getState().filters; + if (filters) { + filterManager.setAppFilters(cloneDeep(filters)); + } + const query = appStateContainer.getState().query; + if (query) { + data.query.queryString.setQuery(query); + } + + const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( + data.query, + appStateContainer, + { + filters: esFilters.FilterStateStore.APP_STATE, + query: true, + } + ); + + // syncs `_g` portion of url with query services + const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl( + data.query, + stateStorage + ); + + replaceUrlAppState({}).then(() => { + start(); + }); + + return () => { + stopSyncingQueryAppStateWithStateContainer(); + stopSyncingGlobalStateWithUrl(); + stop(); + }; + }, }; } diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts index 051a2d2dcd9cc..4c3d819f063a0 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts @@ -62,10 +62,6 @@ describe('test useDiscoverState', () => { }); }); - await act(async () => { - result.current.stateContainer.startSync(); - }); - const initialColumns = result.current.state.columns; await act(async () => { result.current.setState({ columns: ['123'] }); diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index a3546d54cd493..3c736f09a8296 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -6,19 +6,25 @@ * Side Public License, v 1. */ import { useMemo, useEffect, useState, useCallback } from 'react'; -import { cloneDeep } from 'lodash'; +import { isEqual } from 'lodash'; import { History } from 'history'; import { getState } from './discover_state'; import { getStateDefaults } from '../utils/get_state_defaults'; -import { - esFilters, - connectToQueryState, - syncQueryStateWithUrl, - IndexPattern, -} from '../../../../../../data/public'; +import { IndexPattern } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; import { SavedSearch } from '../../../../saved_searches'; import { loadIndexPattern } from '../utils/resolve_index_pattern'; +import { useSavedSearch as useSavedSearchData } from './use_saved_search'; +import { + MODIFY_COLUMNS_ON_SWITCH, + SEARCH_FIELDS_FROM_SOURCE, + SEARCH_ON_PAGE_LOAD_SETTING, + SORT_DEFAULT_ORDER_SETTING, +} from '../../../../../common'; +import { useSearchSession } from './use_search_session'; +import { FetchStatus } from '../../../types'; +import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; export function useDiscoverState({ services, @@ -31,9 +37,11 @@ export function useDiscoverState({ history: History; initialIndexPattern: IndexPattern; }) { - const { uiSettings: config, data, filterManager } = services; + const { uiSettings: config, data, filterManager, indexPatterns } = services; const [indexPattern, setIndexPattern] = useState(initialIndexPattern); const [savedSearch, setSavedSearch] = useState(initialSavedSearch); + const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); + const timefilter = data.query.timefilter.timefilter; const searchSource = useMemo(() => { savedSearch.searchSource.setField('index', indexPattern); @@ -57,73 +65,80 @@ export function useDiscoverState({ [config, data, history, savedSearch, services.core.notifications.toasts] ); - const { appStateContainer, getPreviousAppState } = stateContainer; + const { appStateContainer } = stateContainer; const [state, setState] = useState(appStateContainer.getState()); - useEffect(() => { - if (stateContainer.appStateContainer.getState().index !== indexPattern.id) { - // used index pattern is different than the given by url/state which is invalid - stateContainer.setAppState({ index: indexPattern.id }); - } - // sync initial app filters from state to filterManager - const filters = appStateContainer.getState().filters; - if (filters) { - filterManager.setAppFilters(cloneDeep(filters)); - } - const query = appStateContainer.getState().query; - if (query) { - data.query.queryString.setQuery(query); - } + /** + * Search session logic + */ + const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch }); - const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( - data.query, - appStateContainer, - { - filters: esFilters.FilterStateStore.APP_STATE, - query: true, - } - ); + const initialFetchStatus: FetchStatus = useMemo(() => { + // A saved search is created on every page load, so we check the ID to see if we're loading a + // previously saved search or if it is just transient + const shouldSearchOnPageLoad = + config.get(SEARCH_ON_PAGE_LOAD_SETTING) || + savedSearch.id !== undefined || + timefilter.getRefreshInterval().pause === false || + searchSessionManager.hasSearchSessionIdInURL(); + return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED; + }, [config, savedSearch.id, searchSessionManager, timefilter]); - // syncs `_g` portion of url with query services - const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl( - data.query, - stateContainer.kbnUrlStateStorage - ); + /** + * Data fetching logic + */ + const { data$, refetch$, reset } = useSavedSearchData({ + indexPattern, + initialFetchStatus, + searchSessionManager, + searchSource, + services, + stateContainer, + useNewFieldsApi, + }); + + useEffect(() => { + const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data); return () => { - stopSyncingQueryAppStateWithStateContainer(); - stopSyncingGlobalStateWithUrl(); + stopSync(); }; - }, [ - appStateContainer, - config, - data.query, - data.search.session, - getPreviousAppState, - indexPattern.id, - filterManager, - services.indexPatterns, - stateContainer, - ]); + }, [stateContainer, filterManager, data, indexPattern]); + /** + * Track state changes that should trigger a fetch + */ useEffect(() => { - const unsubscribe = stateContainer.appStateContainer.subscribe(async (nextState) => { + const unsubscribe = appStateContainer.subscribe(async (nextState) => { + const { hideChart, interval, sort, index } = state; + // chart was hidden, now it should be displayed, so data is needed + const chartDisplayChanged = nextState.hideChart !== hideChart && hideChart; + const chartIntervalChanged = nextState.interval !== interval; + const docTableSortChanged = !isEqual(nextState.sort, sort); + const indexPatternChanged = !isEqual(nextState.index, index); // NOTE: this is also called when navigating from discover app to context app - if (nextState.index && state.index !== nextState.index) { - const nextIndexPattern = await loadIndexPattern( - nextState.index, - services.indexPatterns, - config - ); + if (nextState.index && indexPatternChanged) { + /** + * Without resetting the fetch state, e.g. a time column would be displayed when switching + * from a index pattern without to a index pattern with time filter for a brief moment + * That's because appState is updated before savedSearchData$ + * The following line of code catches this, but should be improved + */ + reset(); + const nextIndexPattern = await loadIndexPattern(nextState.index, indexPatterns, config); if (nextIndexPattern) { setIndexPattern(nextIndexPattern.loaded); } } + + if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) { + refetch$.next(); + } setState(nextState); }); return () => unsubscribe(); - }, [config, services.indexPatterns, state.index, stateContainer.appStateContainer, setState]); + }, [config, indexPatterns, appStateContainer, setState, state, refetch$, data$, reset]); const resetSavedSearch = useCallback( async (id?: string) => { @@ -143,13 +158,62 @@ export function useDiscoverState({ [services, indexPattern, config, data, stateContainer, savedSearch.id] ); + /** + * Function triggered when user changes index pattern in the sidebar + */ + const onChangeIndexPattern = useCallback( + async (id: string) => { + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern && indexPattern) { + const nextAppState = getSwitchIndexPatternAppState( + indexPattern, + nextIndexPattern, + state.columns || [], + (state.sort || []) as SortPairArr[], + config.get(MODIFY_COLUMNS_ON_SWITCH), + config.get(SORT_DEFAULT_ORDER_SETTING) + ); + stateContainer.setAppState(nextAppState); + } + }, + [config, indexPattern, indexPatterns, state.columns, state.sort, stateContainer] + ); + /** + * Function triggered when the user changes the query in the search bar + */ + const onUpdateQuery = useCallback( + (_payload, isUpdate?: boolean) => { + if (isUpdate === false) { + searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); + refetch$.next(); + } + }, + [refetch$, searchSessionManager] + ); + + /** + * Initial data fetching, also triggered when index pattern changes + */ + useEffect(() => { + if (!indexPattern) { + return; + } + if (initialFetchStatus === FetchStatus.LOADING) { + refetch$.next(); + } + }, [initialFetchStatus, refetch$, indexPattern, data$]); + return { - state, - setState, - stateContainer, + data$, indexPattern, - searchSource, - savedSearch, + refetch$, resetSavedSearch, + onChangeIndexPattern, + onUpdateQuery, + savedSearch, + searchSource, + setState, + state, + stateContainer, }; } diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts index 5976c8fea5ea4..128c94f284f56 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts @@ -12,9 +12,10 @@ import { discoverServiceMock } from '../../../../__mocks__/services'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { useSavedSearch } from './use_saved_search'; -import { AppState, getState } from './discover_state'; +import { getState } from './discover_state'; import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; import { useDiscoverState } from './use_discover_state'; +import { FetchStatus } from '../../../types'; describe('test useSavedSearch', () => { test('useSavedSearch return is valid', async () => { @@ -28,11 +29,10 @@ describe('test useSavedSearch', () => { const { result } = renderHook(() => { return useSavedSearch({ indexPattern: indexPatternMock, - savedSearch: savedSearchMock, + initialFetchStatus: FetchStatus.LOADING, searchSessionManager, searchSource: savedSearchMock.searchSource.createCopy(), services: discoverServiceMock, - state: {} as AppState, stateContainer, useNewFieldsApi: true, }); @@ -69,11 +69,10 @@ describe('test useSavedSearch', () => { const { result, waitForValueToChange } = renderHook(() => { return useSavedSearch({ indexPattern: indexPatternMock, - savedSearch: savedSearchMock, + initialFetchStatus: FetchStatus.LOADING, searchSessionManager, searchSource: resultState.current.searchSource, services: discoverServiceMock, - state: {} as AppState, stateContainer, useNewFieldsApi: true, }); @@ -88,4 +87,47 @@ describe('test useSavedSearch', () => { expect(result.current.data$.value.hits).toBe(0); expect(result.current.data$.value.rows).toEqual([]); }); + + test('reset sets back to initial state', async () => { + const { history, searchSessionManager } = createSearchSessionMock(); + const stateContainer = getState({ + getStateDefaults: () => ({ index: 'the-index-pattern-id' }), + history, + uiSettings: uiSettingsMock, + }); + + discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => { + return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' }; + }); + + const { result: resultState } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + initialIndexPattern: indexPatternMock, + initialSavedSearch: savedSearchMock, + }); + }); + + const { result, waitForValueToChange } = renderHook(() => { + return useSavedSearch({ + indexPattern: indexPatternMock, + initialFetchStatus: FetchStatus.LOADING, + searchSessionManager, + searchSource: resultState.current.searchSource, + services: discoverServiceMock, + stateContainer, + useNewFieldsApi: true, + }); + }); + + result.current.refetch$.next(); + + await waitForValueToChange(() => { + return result.current.data$.value.state === FetchStatus.COMPLETE; + }); + + result.current.reset(); + expect(result.current.data$.value.state).toBe(FetchStatus.LOADING); + }); }); diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts index 2b0d951724869..8c847b54078eb 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts @@ -5,11 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { useEffect, useRef, useCallback, useMemo } from 'react'; +import { useEffect, useRef, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { merge, Subject, BehaviorSubject } from 'rxjs'; import { debounceTime, tap, filter } from 'rxjs/operators'; -import { isEqual } from 'lodash'; import { DiscoverServices } from '../../../../build_services'; import { DiscoverSearchSessionManager } from './discover_search_session'; import { @@ -18,13 +17,11 @@ import { SearchSource, tabifyAggResponse, } from '../../../../../../data/common'; -import { SavedSearch } from '../../../../saved_searches'; -import { AppState, GetStateReturn } from './discover_state'; +import { GetStateReturn } from './discover_state'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; import { RequestAdapter } from '../../../../../../inspector/public'; import { AutoRefreshDoneFn, search } from '../../../../../../data/public'; import { calcFieldCounts } from '../utils/calc_field_counts'; -import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../../common'; import { validateTimeRange } from '../utils/validate_time_range'; import { updateSearchSource } from '../utils/update_search_source'; import { SortOrder } from '../../../../saved_searches/types'; @@ -40,6 +37,7 @@ export type SavedSearchRefetchSubject = Subject; export interface UseSavedSearch { refetch$: SavedSearchRefetchSubject; data$: SavedSearchDataSubject; + reset: () => void; } export type SavedSearchRefetchMsg = 'reset' | undefined; @@ -59,48 +57,27 @@ export interface SavedSearchDataMessage { /** * This hook return 2 observables, refetch$ allows to trigger data fetching, data$ to subscribe * to the data fetching - * @param indexPattern - * @param savedSearch - * @param searchSessionManager - * @param searchSource - * @param services - * @param state - * @param stateContainer - * @param useNewFieldsApi */ export const useSavedSearch = ({ indexPattern, - savedSearch, + initialFetchStatus, searchSessionManager, searchSource, services, - state, stateContainer, useNewFieldsApi, }: { indexPattern: IndexPattern; - savedSearch: SavedSearch; + initialFetchStatus: FetchStatus; searchSessionManager: DiscoverSearchSessionManager; searchSource: SearchSource; services: DiscoverServices; - state: AppState; stateContainer: GetStateReturn; useNewFieldsApi: boolean; }): UseSavedSearch => { - const { data, filterManager, uiSettings } = services; + const { data, filterManager } = services; const timefilter = data.query.timefilter.timefilter; - const initFetchState: FetchStatus = useMemo(() => { - // A saved search is created on every page load, so we check the ID to see if we're loading a - // previously saved search or if it is just transient - const shouldSearchOnPageLoad = - uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || - savedSearch.id !== undefined || - timefilter.getRefreshInterval().pause === false || - searchSessionManager.hasSearchSessionIdInURL(); - return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED; - }, [uiSettings, savedSearch.id, searchSessionManager, timefilter]); - /** * The observable the UI (aka React component) subscribes to get notified about * the changes in the data fetching process (high level: fetching started, data was received) @@ -108,7 +85,7 @@ export const useSavedSearch = ({ const data$: SavedSearchDataSubject = useSingleton( () => new BehaviorSubject({ - state: initFetchState, + state: initialFetchStatus, }) ); /** @@ -123,15 +100,14 @@ export const useSavedSearch = ({ */ const refs = useRef<{ abortController?: AbortController; - /** - * used to compare a new state against an old one, to evaluate if data needs to be fetched - */ - appState: AppState; /** * handler emitted by `timefilter.getAutoRefreshFetch$()` * to notify when data completed loading and to start a new autorefresh loop */ autoRefreshDoneCb?: AutoRefreshDoneFn; + /** + * Number of fetches used for functional testing + */ fetchCounter: number; /** * needed to right auto refresh behavior, a new auto refresh shouldnt be triggered when @@ -144,12 +120,34 @@ export const useSavedSearch = ({ */ fieldCounts: Record; }>({ - appState: state, fetchCounter: 0, fieldCounts: {}, - fetchStatus: initFetchState, + fetchStatus: initialFetchStatus, }); + /** + * Resets the fieldCounts cache and sends a reset message + * It is set to initial state (no documents, fetchCounter to 0) + * Needed when index pattern is switched or a new runtime field is added + */ + const sendResetMsg = useCallback( + (fetchStatus?: FetchStatus) => { + refs.current.fieldCounts = {}; + refs.current.fetchStatus = fetchStatus ?? initialFetchStatus; + data$.next({ + state: initialFetchStatus, + fetchCounter: 0, + rows: [], + fieldCounts: {}, + chartData: undefined, + bucketInterval: undefined, + }); + }, + [data$, initialFetchStatus] + ); + /** + * Function to fetch data from ElasticSearch + */ const fetchAll = useCallback( (reset = false) => { if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) { @@ -161,23 +159,18 @@ export const useSavedSearch = ({ refs.current.abortController = new AbortController(); const sessionId = searchSessionManager.getNextSearchSessionId(); - // Let the UI know, data fetching started - const loadingMessage: SavedSearchDataMessage = { - state: FetchStatus.LOADING, - fetchCounter: ++refs.current.fetchCounter, - }; - if (reset) { - // when runtime field was added, changed, deleted, index pattern was switched - loadingMessage.rows = []; - loadingMessage.fieldCounts = {}; - loadingMessage.chartData = undefined; - loadingMessage.bucketInterval = undefined; + sendResetMsg(FetchStatus.LOADING); + } else { + // Let the UI know, data fetching started + data$.next({ + state: FetchStatus.LOADING, + fetchCounter: ++refs.current.fetchCounter, + }); + refs.current.fetchStatus = FetchStatus.LOADING; } - data$.next(loadingMessage); - refs.current.fetchStatus = loadingMessage.state; - const { sort } = stateContainer.appStateContainer.getState(); + const { sort, hideChart, interval } = stateContainer.appStateContainer.getState(); updateSearchSource(searchSource, false, { indexPattern, services, @@ -185,8 +178,8 @@ export const useSavedSearch = ({ useNewFieldsApi, }); const chartAggConfigs = - indexPattern.timeFieldName && !state.hideChart && state.interval - ? getChartAggConfigs(searchSource, state.interval, data) + indexPattern.timeFieldName && !hideChart && interval + ? getChartAggConfigs(searchSource, interval, data) : undefined; if (!chartAggConfigs) { @@ -217,16 +210,12 @@ export const useSavedSearch = ({ state: FetchStatus.COMPLETE, rows: documents, inspectorAdapters, - fieldCounts: calcFieldCounts( - reset ? {} : refs.current.fieldCounts, - documents, - indexPattern - ), + fieldCounts: calcFieldCounts(refs.current.fieldCounts, documents, indexPattern), hits: res.rawResponse.hits.total as number, }; if (chartAggConfigs) { - const bucketAggConfig = chartAggConfigs!.aggs[1]; + const bucketAggConfig = chartAggConfigs.aggs[1]; const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse); const dimensions = getDimensions(chartAggConfigs, data); if (dimensions) { @@ -259,14 +248,13 @@ export const useSavedSearch = ({ [ timefilter, services, + searchSessionManager, stateContainer.appStateContainer, searchSource, indexPattern, useNewFieldsApi, - state.hideChart, - state.interval, data, - searchSessionManager, + sendResetMsg, data$, ] ); @@ -306,32 +294,9 @@ export const useSavedSearch = ({ fetchAll, ]); - /** - * Track state changes that should trigger a fetch - */ - useEffect(() => { - const prevAppState = refs.current.appState; - - // chart was hidden, now it should be displayed, so data is needed - const chartDisplayChanged = state.hideChart !== prevAppState.hideChart && !state.hideChart; - const chartIntervalChanged = state.interval !== prevAppState.interval; - const docTableSortChanged = !isEqual(state.sort, prevAppState.sort); - const indexPatternChanged = !isEqual(state.index, prevAppState.index); - - refs.current.appState = state; - if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged || indexPatternChanged) { - refetch$.next(indexPatternChanged ? 'reset' : undefined); - } - }, [refetch$, state.interval, state.sort, state]); - - useEffect(() => { - if (initFetchState === FetchStatus.LOADING) { - refetch$.next(); - } - }, [initFetchState, refetch$]); - return { refetch$, data$, + reset: sendResetMsg, }; }; From fd15a1f5f8fad4196952724ac740658a313bb4bc Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:31:59 -0400 Subject: [PATCH 067/118] [Security Solution][Endpoint] Rename `Unisolating` and other like words to `Releasing` (#102582) (#102960) * Update all instances of `unisolate` to `release` (along with variation of unisolate) * just width of Agent Status column on endpoint list Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> --- .../host_isolation/endpoint_host_isolation_status.test.tsx | 4 ++-- .../host_isolation/endpoint_host_isolation_status.tsx | 6 +++--- .../components/endpoint/host_isolation/translations.ts | 6 +++--- .../detections/components/host_isolation/translations.ts | 2 +- .../view/components/endpoint_agent_status.test.tsx | 4 +--- .../endpoint_hosts/view/hooks/use_endpoint_action_items.tsx | 2 +- .../public/management/pages/endpoint_hosts/view/index.tsx | 4 ++-- .../management/pages/endpoint_hosts/view/translations.ts | 6 +++--- 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx index 44405748b6373..4ceacc40942e2 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx @@ -44,8 +44,8 @@ describe('when using the EndpointHostIsolationStatus component', () => { }); it.each([ - ['Isolating pending', { pendingIsolate: 2 }], - ['Unisolating pending', { pendingUnIsolate: 2 }], + ['Isolating', { pendingIsolate: 2 }], + ['Releasing', { pendingUnIsolate: 2 }], ['4 actions pending', { isIsolated: true, pendingUnIsolate: 2, pendingIsolate: 2 }], ])('should show %s}', (expectedLabel, componentProps) => { const { getByTestId } = render(componentProps); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx index 0fe3a8e4337cb..7ae7cae89f19e 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx @@ -74,7 +74,7 @@ export const EndpointHostIsolationStatus = memo {pendingUnIsolate} @@ -101,12 +101,12 @@ export const EndpointHostIsolationStatus = memo ) : ( )} diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts index 790c951f61ccd..66d9bf3a7c71b 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts @@ -26,13 +26,13 @@ export const COMMENT_PLACEHOLDER = i18n.translate( export const GET_ISOLATION_SUCCESS_MESSAGE = (hostName: string) => i18n.translate('xpack.securitySolution.endpoint.hostIsolation.isolation.successfulMessage', { - defaultMessage: 'Host Isolation on {hostName} successfully submitted', + defaultMessage: 'Isolation on host {hostName} successfully submitted', values: { hostName }, }); export const GET_UNISOLATION_SUCCESS_MESSAGE = (hostName: string) => i18n.translate('xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage', { - defaultMessage: 'Host Unisolation on {hostName} successfully submitted', + defaultMessage: 'Release on host {hostName} successfully submitted', values: { hostName }, }); @@ -41,7 +41,7 @@ export const ISOLATE = i18n.translate('xpack.securitySolution.endpoint.hostisola }); export const UNISOLATE = i18n.translate('xpack.securitySolution.endpoint.hostisolation.unisolate', { - defaultMessage: 'unisolate', + defaultMessage: 'release', }); export const NOT_ISOLATED = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts index 98b74817cabb6..58667c26ce2e6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts @@ -17,7 +17,7 @@ export const ISOLATE_HOST = i18n.translate( export const UNISOLATE_HOST = i18n.translate( 'xpack.securitySolution.endpoint.hostIsolation.unisolateHost', { - defaultMessage: 'Unisolate host', + defaultMessage: 'Release host', } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx index 9010bb5785c1d..a860e3c45deee 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx @@ -82,9 +82,7 @@ describe('When using the EndpointAgentStatus component', () => { }); it('should show host pending action', () => { - expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual( - 'Isolating pending' - ); + expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual('Isolating'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 7c38c935a0b9f..408e1794ef680 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -76,7 +76,7 @@ export const useEndpointActionItems = ( children: ( ), }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index d1dab3dd07a7e..9316d2539d133 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -272,7 +272,7 @@ export const EndpointList = () => { }, { field: 'host_status', - width: '9%', + width: '14%', name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', { defaultMessage: 'Agent Status', }), @@ -356,7 +356,7 @@ export const EndpointList = () => { }, { field: 'metadata.host.os.name', - width: '10%', + width: '9%', name: i18n.translate('xpack.securitySolution.endpoint.list.os', { defaultMessage: 'Operating System', }), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index 1a7889f22db16..18a5bd1e5130a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -26,7 +26,7 @@ export const ACTIVITY_LOG = { unisolatedAction: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.unisolated', { - defaultMessage: 'unisolated host', + defaultMessage: 'released host', } ), }, @@ -46,13 +46,13 @@ export const ACTIVITY_LOG = { unisolationSuccessful: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful', { - defaultMessage: 'host unisolation successful', + defaultMessage: 'host release successful', } ), unisolationFailed: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationFailed', { - defaultMessage: 'host unisolation failed', + defaultMessage: 'host release failed', } ), }, From 0e28661990ee475f232ef9d22e51bd771cc8c277 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Tue, 22 Jun 2021 19:44:35 +0100 Subject: [PATCH 068/118] [Discover] Add source to doc viewer (#101392) (#102962) * [Discover] Add source to doc viewer * Updating a unit test * Fix typescript errors * Add unit test * Add a functional test * Fixing a typo * Remove unnecessary import * Always request fields and source * Remove unused import * Move initialization of SourceViewer back to setup * Trying to get rid of null value * Readding null * Try to get rid of null value * Addressing PR comments * Return early if jsonValue is not set * Fix loading spinner style * Add refresh on error * Fix error message * Add loading indicator on an empty string Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/doc/doc.tsx | 7 +- .../components/doc/elastic_request_state.ts | 15 + .../components/doc/use_es_doc_search.test.tsx | 39 +- .../components/doc/use_es_doc_search.ts | 84 +- .../json_code_editor.test.tsx.snap | 60 +- .../json_code_editor/json_code_editor.scss | 2 +- .../json_code_editor/json_code_editor.tsx | 59 +- .../json_code_editor_common.tsx | 86 ++ .../__snapshots__/source_viewer.test.tsx.snap | 760 ++++++++++++++++++ .../source_viewer/source_viewer.scss | 14 + .../source_viewer/source_viewer.test.tsx | 118 +++ .../source_viewer/source_viewer.tsx | 129 +++ src/plugins/discover/public/plugin.tsx | 15 +- .../apps/discover/_discover_fields_api.ts | 9 + test/functional/page_objects/discover_page.ts | 8 + 15 files changed, 1248 insertions(+), 157 deletions(-) create mode 100644 src/plugins/discover/public/application/components/doc/elastic_request_state.ts create mode 100644 src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx create mode 100644 src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap create mode 100644 src/plugins/discover/public/application/components/source_viewer/source_viewer.scss create mode 100644 src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx create mode 100644 src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx index e38709b465174..ed8bcf30d2bd1 100644 --- a/src/plugins/discover/public/application/components/doc/doc.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.tsx @@ -10,9 +10,10 @@ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; -import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; +import { useEsDocSearch } from './use_es_doc_search'; import { getServices } from '../../../kibana_services'; import { DocViewer } from '../doc_viewer/doc_viewer'; +import { ElasticRequestState } from './elastic_request_state'; export interface DocProps { /** @@ -32,6 +33,10 @@ export interface DocProps { * IndexPatternService to get a given index pattern by ID */ indexPatternService: IndexPatternsContract; + /** + * If set, will always request source, regardless of the global `fieldsFromSource` setting + */ + requestSource?: boolean; } export function Doc(props: DocProps) { diff --git a/src/plugins/discover/public/application/components/doc/elastic_request_state.ts b/src/plugins/discover/public/application/components/doc/elastic_request_state.ts new file mode 100644 index 0000000000000..241e37c47a7e7 --- /dev/null +++ b/src/plugins/discover/public/application/components/doc/elastic_request_state.ts @@ -0,0 +1,15 @@ +/* + * 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 enum ElasticRequestState { + Loading, + NotFound, + Found, + Error, + NotFoundIndexPattern, +} diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx index f3a6b274649f5..9fdb564cb518d 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx @@ -7,11 +7,12 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search'; +import { buildSearchBody, useEsDocSearch } from './use_es_doc_search'; import { DocProps } from './doc'; import { Observable } from 'rxjs'; import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; import { IndexPattern } from 'src/plugins/data/common'; +import { ElasticRequestState } from './elastic_request_state'; const mockSearchResult = new Observable(); @@ -88,6 +89,36 @@ describe('Test of helper / hook', () => { `); }); + test('buildSearchBody with requestSource', () => { + const indexPattern = ({ + getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), + } as unknown) as IndexPattern; + const actual = buildSearchBody('1', indexPattern, true, true); + expect(actual).toMatchInlineSnapshot(` + Object { + "body": Object { + "_source": true, + "fields": Array [ + Object { + "field": "*", + "include_unmapped": "true", + }, + ], + "query": Object { + "ids": Object { + "values": Array [ + "1", + ], + }, + }, + "runtime_mappings": Object {}, + "script_fields": Array [], + "stored_fields": Array [], + }, + } + `); + }); + test('buildSearchBody with runtime fields', () => { const indexPattern = ({ getComputedFields: () => ({ @@ -155,7 +186,11 @@ describe('Test of helper / hook', () => { await act(async () => { hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props }); }); - expect(hook.result.current).toEqual([ElasticRequestState.Loading, null, indexPattern]); + expect(hook.result.current.slice(0, 3)).toEqual([ + ElasticRequestState.Loading, + null, + indexPattern, + ]); expect(getMock).toHaveBeenCalled(); }); }); diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts index 7a3320d43c8b5..71a32b758aca7 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts @@ -6,23 +6,16 @@ * Side Public License, v 1. */ -import { useEffect, useState, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import type { estypes } from '@elastic/elasticsearch'; -import { IndexPattern, getServices } from '../../../kibana_services'; +import { getServices, IndexPattern } from '../../../kibana_services'; import { DocProps } from './doc'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; +import { ElasticRequestState } from './elastic_request_state'; type RequestBody = Pick; -export enum ElasticRequestState { - Loading, - NotFound, - Found, - Error, - NotFoundIndexPattern, -} - /** * helper function to build a query body for Elasticsearch * https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html @@ -30,7 +23,8 @@ export enum ElasticRequestState { export function buildSearchBody( id: string, indexPattern: IndexPattern, - useNewFieldsApi: boolean + useNewFieldsApi: boolean, + requestAllFields?: boolean ): RequestBody | undefined { const computedFields = indexPattern.getComputedFields(); const runtimeFields = computedFields.runtimeFields as estypes.MappingRuntimeFields; @@ -52,6 +46,9 @@ export function buildSearchBody( // @ts-expect-error request.body.fields = [{ field: '*', include_unmapped: 'true' }]; request.body.runtime_mappings = runtimeFields ? runtimeFields : {}; + if (requestAllFields) { + request.body._source = true; + } } else { request.body._source = true; } @@ -67,47 +64,50 @@ export function useEsDocSearch({ index, indexPatternId, indexPatternService, -}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null] { + requestSource, +}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null, () => void] { const [indexPattern, setIndexPattern] = useState(null); const [status, setStatus] = useState(ElasticRequestState.Loading); const [hit, setHit] = useState(null); const { data, uiSettings } = useMemo(() => getServices(), []); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); - useEffect(() => { - async function requestData() { - try { - const indexPatternEntity = await indexPatternService.get(indexPatternId); - setIndexPattern(indexPatternEntity); + const requestData = useCallback(async () => { + try { + const indexPatternEntity = await indexPatternService.get(indexPatternId); + setIndexPattern(indexPatternEntity); - const { rawResponse } = await data.search - .search({ - params: { - index, - body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi)?.body, - }, - }) - .toPromise(); + const { rawResponse } = await data.search + .search({ + params: { + index, + body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi, requestSource)?.body, + }, + }) + .toPromise(); - const hits = rawResponse.hits; + const hits = rawResponse.hits; - if (hits?.hits?.[0]) { - setStatus(ElasticRequestState.Found); - setHit(hits.hits[0]); - } else { - setStatus(ElasticRequestState.NotFound); - } - } catch (err) { - if (err.savedObjectId) { - setStatus(ElasticRequestState.NotFoundIndexPattern); - } else if (err.status === 404) { - setStatus(ElasticRequestState.NotFound); - } else { - setStatus(ElasticRequestState.Error); - } + if (hits?.hits?.[0]) { + setStatus(ElasticRequestState.Found); + setHit(hits.hits[0]); + } else { + setStatus(ElasticRequestState.NotFound); + } + } catch (err) { + if (err.savedObjectId) { + setStatus(ElasticRequestState.NotFoundIndexPattern); + } else if (err.status === 404) { + setStatus(ElasticRequestState.NotFound); + } else { + setStatus(ElasticRequestState.Error); } } + }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi, requestSource]); + + useEffect(() => { requestData(); - }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi]); - return [status, hit, indexPattern]; + }, [requestData]); + + return [status, hit, indexPattern, requestData]; } diff --git a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap index 8f07614813495..31dd6347218b5 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap +++ b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap @@ -1,21 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`returns the \`JsonCodeEditor\` component 1`] = ` - - - -
- - - -
-
- - - -
+ onEditorDidMount={[Function]} +/> `; diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss index 5521df5b363ac..335805ed28493 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss @@ -1,3 +1,3 @@ .dscJsonCodeEditor { - width: 100% + width: 100%; } diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx index b8427bb6bbdd2..f1ecd3ae3b70b 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx @@ -9,17 +9,8 @@ import './json_code_editor.scss'; import React, { useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { monaco, XJsonLang } from '@kbn/monaco'; -import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { CodeEditor } from '../../../../../kibana_react/public'; - -const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { - defaultMessage: 'Read only JSON view of an elasticsearch document', -}); -const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', { - defaultMessage: 'Copy to clipboard', -}); +import { monaco } from '@kbn/monaco'; +import { JsonCodeEditorCommon } from './json_code_editor_common'; interface JsonCodeEditorProps { json: Record; @@ -47,45 +38,11 @@ export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorPr }, []); return ( - - - -
- - {(copy) => ( - - {copyToClipboardLabel} - - )} - -
-
- - {}} - editorDidMount={setEditorCalculatedHeight} - aria-label={codeEditorAriaLabel} - options={{ - automaticLayout: true, - fontSize: 12, - lineNumbers: hasLineNumbers ? 'on' : 'off', - minimap: { - enabled: false, - }, - overviewRulerBorder: false, - readOnly: true, - scrollbar: { - alwaysConsumeMouseWheel: false, - }, - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', - }} - /> - -
+ ); }; diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx new file mode 100644 index 0000000000000..e5ab8bf4d1a0d --- /dev/null +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './json_code_editor.scss'; + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { monaco, XJsonLang } from '@kbn/monaco'; +import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { CodeEditor } from '../../../../../kibana_react/public'; + +const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { + defaultMessage: 'Read only JSON view of an elasticsearch document', +}); +const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', { + defaultMessage: 'Copy to clipboard', +}); + +interface JsonCodeEditorCommonProps { + jsonValue: string; + onEditorDidMount: (editor: monaco.editor.IStandaloneCodeEditor) => void; + width?: string | number; + hasLineNumbers?: boolean; +} + +export const JsonCodeEditorCommon = ({ + jsonValue, + width, + hasLineNumbers, + onEditorDidMount, +}: JsonCodeEditorCommonProps) => { + if (jsonValue === '') { + return null; + } + return ( + + + +
+ + {(copy) => ( + + {copyToClipboardLabel} + + )} + +
+
+ + {}} + editorDidMount={onEditorDidMount} + aria-label={codeEditorAriaLabel} + options={{ + automaticLayout: true, + fontSize: 12, + lineNumbers: hasLineNumbers ? 'on' : 'off', + minimap: { + enabled: false, + }, + overviewRulerBorder: false, + readOnly: true, + scrollbar: { + alwaysConsumeMouseWheel: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + }} + /> + +
+ ); +}; + +export const JSONCodeEditorCommonMemoized = React.memo((props: JsonCodeEditorCommonProps) => { + return ; +}); diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap new file mode 100644 index 0000000000000..f40dbbbae1f87 --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -0,0 +1,760 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Source Viewer component renders error state 1`] = ` + + + Could not fetch data at this time. Refresh the tab to try again. + + + Refresh + +

+ } + iconType="alert" + title={ +

+ An Error Occurred +

+ } + > +
+ + + + +
+ + + + +

+ An Error Occurred +

+
+ +
+ + +
+
+ Could not fetch data at this time. Refresh the tab to try again. + +
+ + + + + + +
+
+ + + +
+ + +`; + +exports[`Source Viewer component renders json code editor 1`] = ` + + + + +
+ +
+ +
+ +
+ + + + + + + + + +
+
+ + +
+ + + } + > + + + + + + +
+
+
+ + + + +`; + +exports[`Source Viewer component renders loading state 1`] = ` + +
+ + + + +
+ +
+ + Loading JSON + +
+
+
+
+
+
+`; diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss new file mode 100644 index 0000000000000..224e84ca50b52 --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss @@ -0,0 +1,14 @@ +.sourceViewer__loading { + display: flex; + flex-direction: row; + justify-content: left; + flex: 1 0 100%; + text-align: center; + height: 100%; + width: 100%; + margin-top: $euiSizeS; +} + +.sourceViewer__loadingSpinner { + margin-right: $euiSizeS; +} diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx new file mode 100644 index 0000000000000..86433e5df6401 --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { SourceViewer } from './source_viewer'; +import * as hooks from '../doc/use_es_doc_search'; +import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { JsonCodeEditorCommon } from '../json_code_editor/json_code_editor_common'; + +jest.mock('../../../kibana_services', () => ({ + getServices: jest.fn(), +})); + +import { getServices, IndexPattern } from '../../../kibana_services'; + +const mockIndexPattern = { + getComputedFields: () => [], +} as never; +const getMock = jest.fn(() => Promise.resolve(mockIndexPattern)); +const mockIndexPatternService = ({ + get: getMock, +} as unknown) as IndexPattern; + +(getServices as jest.Mock).mockImplementation(() => ({ + uiSettings: { + get: (key: string) => { + if (key === 'discover:useNewFieldsApi') { + return true; + } + }, + }, + data: { + indexPatternService: mockIndexPatternService, + }, +})); +describe('Source Viewer component', () => { + test('renders loading state', () => { + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, null, () => {}]); + + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const loadingIndicator = comp.find(EuiLoadingSpinner); + expect(loadingIndicator).not.toBe(null); + }); + + test('renders error state', () => { + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, null, () => {}]); + + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const errorPrompt = comp.find(EuiEmptyPrompt); + expect(errorPrompt.length).toBe(1); + const refreshButton = comp.find(EuiButton); + expect(refreshButton.length).toBe(1); + }); + + test('renders json code editor', () => { + const mockHit = { + _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', + _score: 1, + _source: { + message: 'Lorem ipsum dolor sit amet', + extension: 'html', + not_mapped: 'yes', + bytes: 100, + objectArray: [{ foo: true }], + relatedContent: { + test: 1, + }, + scripted: 123, + _underscore: 123, + }, + } as never; + jest + .spyOn(hooks, 'useEsDocSearch') + .mockImplementation(() => [2, mockHit, mockIndexPattern, () => {}]); + jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => { + return false; + }); + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const jsonCodeEditor = comp.find(JsonCodeEditorCommon); + expect(jsonCodeEditor).not.toBe(null); + }); +}); diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx new file mode 100644 index 0000000000000..94a12c04613a9 --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import './source_viewer.scss'; + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { monaco } from '@kbn/monaco'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useEsDocSearch } from '../doc/use_es_doc_search'; +import { JSONCodeEditorCommonMemoized } from '../json_code_editor/json_code_editor_common'; +import { ElasticRequestState } from '../doc/elastic_request_state'; +import { getServices } from '../../../../public/kibana_services'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; + +interface SourceViewerProps { + id: string; + index: string; + indexPatternId: string; + hasLineNumbers: boolean; + width?: number; +} + +export const SourceViewer = ({ + id, + index, + indexPatternId, + width, + hasLineNumbers, +}: SourceViewerProps) => { + const [editor, setEditor] = useState(); + const [jsonValue, setJsonValue] = useState(''); + const indexPatternService = getServices().data.indexPatterns; + const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const [reqState, hit, , requestData] = useEsDocSearch({ + id, + index, + indexPatternId, + indexPatternService, + requestSource: useNewFieldsApi, + }); + + useEffect(() => { + if (reqState === ElasticRequestState.Found && hit) { + setJsonValue(JSON.stringify(hit, undefined, 2)); + } + }, [reqState, hit]); + + // setting editor height based on lines height and count to stretch and fit its content + useEffect(() => { + if (!editor) { + return; + } + const editorElement = editor.getDomNode(); + + if (!editorElement) { + return; + } + + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const lineCount = editor.getModel()?.getLineCount() || 1; + const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight; + if (!jsonValue || jsonValue === '') { + editorElement.style.height = '0px'; + } else { + editorElement.style.height = `${height}px`; + } + editor.layout(); + }, [editor, jsonValue]); + + const loadingState = ( +
+ + + + +
+ ); + + const errorMessageTitle = ( +

+ {i18n.translate('discover.sourceViewer.errorMessageTitle', { + defaultMessage: 'An Error Occurred', + })} +

+ ); + const errorMessage = ( +
+ {i18n.translate('discover.sourceViewer.errorMessage', { + defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.', + })} + + + {i18n.translate('discover.sourceViewer.refresh', { + defaultMessage: 'Refresh', + })} + +
+ ); + const errorState = ( + + ); + + if ( + reqState === ElasticRequestState.Error || + reqState === ElasticRequestState.NotFound || + reqState === ElasticRequestState.NotFoundIndexPattern + ) { + return errorState; + } + + if (reqState === ElasticRequestState.Loading || jsonValue === '') { + return loadingState; + } + + return ( + setEditor(editorNode)} + /> + ); +}; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 139b23d28a1d4..7b4e7bb67c00e 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -37,7 +37,7 @@ import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; import { DocViewTable } from './application/components/table/table'; -import { JsonCodeEditor } from './application/components/json_code_editor/json_code_editor'; + import { setDocViewsRegistry, setUrlTracker, @@ -63,6 +63,7 @@ import { SearchEmbeddableFactory } from './application/embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { replaceUrlHashQuery } from '../../kibana_utils/public/'; import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public'; +import { SourceViewer } from './application/components/source_viewer/source_viewer'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -178,7 +179,6 @@ export class DiscoverPlugin }) ); } - this.docViewsRegistry = new DocViewsRegistry(); setDocViewsRegistry(this.docViewsRegistry); this.docViewsRegistry.addDocView({ @@ -193,8 +193,14 @@ export class DiscoverPlugin defaultMessage: 'JSON', }), order: 20, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: ({ hit }) => , + component: ({ hit, indexPattern }) => ( + + ), }); const { @@ -273,6 +279,7 @@ export class DiscoverPlugin // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); + const { renderApp } = await import('./application/application'); params.element.classList.add('dscAppWrapper'); const unmount = await renderApp(innerAngularName, params.element); diff --git a/test/functional/apps/discover/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts index 614a0794ffb3b..42e2a94b36462 100644 --- a/test/functional/apps/discover/_discover_fields_api.ts +++ b/test/functional/apps/discover/_discover_fields_api.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from './ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); + const docTable = getService('docTable'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -58,5 +59,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score'); expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); }); + + it('displays _source viewer in doc viewer', async function () { + await docTable.clickRowToggle({ rowIndex: 0 }); + + await PageObjects.discover.isShowingDocViewer(); + await PageObjects.discover.clickDocViewerTab(1); + await PageObjects.discover.expectSourceViewerToExist(); + }); }); } diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 3f3857d634254..eca42fa44dff4 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -296,6 +296,14 @@ export class DiscoverPageObject extends FtrService { return await this.testSubjects.exists('kbnDocViewer'); } + public async clickDocViewerTab(index: number) { + return await this.find.clickByCssSelector(`#kbn_doc_viewer_tab_${index}`); + } + + public async expectSourceViewerToExist() { + return await this.find.byClassName('monaco-editor'); + } + public async getMarks() { const table = await this.docTable.getTable(); const marks = await table.findAllByTagName('mark'); From 617538433d529ffdd60e0703a6aca446fb77a9b9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:48:23 -0400 Subject: [PATCH 069/118] remove duplicate apm-rum deps from devDeps (#102838) (#102963) Co-authored-by: spalger Co-authored-by: Spencer Co-authored-by: spalger --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 83c28adfb6314..01b4a4f58de17 100644 --- a/package.json +++ b/package.json @@ -443,8 +443,6 @@ "@bazel/typescript": "^3.5.1", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", - "@elastic/apm-rum": "^5.6.1", - "@elastic/apm-rum-react": "^1.2.5", "@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", From d00d0d2a55819adff14000b1095165daa8433fa0 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:53:42 -0400 Subject: [PATCH 070/118] [Security Solution][Endpoint] Don't create event filters list from manifest manager (#102618) (#102964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Check if endpoint event filters list exists before create and create it without specific id * Removes creation of endpoint event filters list in manifest manager Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: David Sánchez --- .../create_endoint_event_filters_list.ts | 82 ------------------- .../exception_lists/exception_list_client.ts | 13 --- .../server/endpoint/lib/artifacts/lists.ts | 2 - 3 files changed, 97 deletions(-) delete mode 100644 x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts deleted file mode 100644 index 94a049d10cc45..0000000000000 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts +++ /dev/null @@ -1,82 +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 { SavedObjectsClientContract } from 'kibana/server'; -import uuid from 'uuid'; -import { Version } from '@kbn/securitysolution-io-ts-types'; -import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; -import { - ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_EVENT_FILTERS_LIST_NAME, -} from '@kbn/securitysolution-list-constants'; - -import { ExceptionListSoSchema } from '../../schemas/saved_objects'; - -import { transformSavedObjectToExceptionList } from './utils'; - -interface CreateEndpointEventFiltersListOptions { - savedObjectsClient: SavedObjectsClientContract; - user: string; - tieBreaker?: string; - version: Version; -} - -/** - * Creates the Endpoint Trusted Apps agnostic list if it does not yet exist - * - * @param savedObjectsClient - * @param user - * @param tieBreaker - * @param version - */ -export const createEndpointEventFiltersList = async ({ - savedObjectsClient, - user, - tieBreaker, - version, -}: CreateEndpointEventFiltersListOptions): Promise => { - const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' }); - const dateNow = new Date().toISOString(); - try { - const savedObject = await savedObjectsClient.create( - savedObjectType, - { - comments: undefined, - created_at: dateNow, - created_by: user, - description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, - entries: undefined, - immutable: false, - item_id: undefined, - list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, - list_type: 'list', - meta: undefined, - name: ENDPOINT_EVENT_FILTERS_LIST_NAME, - os_types: [], - tags: [], - tie_breaker_id: tieBreaker ?? uuid.v4(), - type: 'endpoint_events', - updated_by: user, - version, - }, - { - // We intentionally hard coding the id so that there can only be one Event Filters list within the space - id: ENDPOINT_EVENT_FILTERS_LIST_ID, - } - ); - - return transformSavedObjectToExceptionList({ savedObject }); - } catch (err) { - if (savedObjectsClient.errors.isConflictError(err)) { - return null; - } else { - throw err; - } - } -}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 4ccff2dd000b9..77e82bf0f7578 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -54,7 +54,6 @@ import { } from './find_exception_list_items'; import { createEndpointList } from './create_endpoint_list'; import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list'; -import { createEndpointEventFiltersList } from './create_endoint_event_filters_list'; export class ExceptionListClient { private readonly user: string; @@ -120,18 +119,6 @@ export class ExceptionListClient { }); }; - /** - * Create the Endpoint Event Filters Agnostic list if it does not yet exist (`null` is returned if it does exist) - */ - public createEndpointEventFiltersList = async (): Promise => { - const { savedObjectsClient, user } = this; - return createEndpointEventFiltersList({ - savedObjectsClient, - user, - version: 1, - }); - }; - /** * This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will * auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index f5d3b30bf15fa..e27a09efd9710 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -140,8 +140,6 @@ export async function getEndpointEventFiltersList( policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : '' })`; - await eClient.createEndpointEventFiltersList(); - return getFilteredEndpointExceptionList( eClient, schemaVersion, From be8aea67865c2df60c4d026b57e59a4464dbe505 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:54:55 -0400 Subject: [PATCH 071/118] [7.x] [jest] use circus runner for the integration tests (#102782) (#102948) * [jest] use circus runner for the integration tests (#102782) * use circus runner for integration tests * do not use done callback. https://github.com/facebook/jest/issues/10529 * fix type error * disable reporting for so 100k migration Co-authored-by: Mikhail Shustov --- jest.config.integration.js | 1 - .../http/integration_tests/request.test.ts | 51 ++++++++++--------- .../integration_tests/migration.test.ts | 6 +++ .../migration_7.7.2_xpack_100k.test.ts | 6 +++ .../integration_tests/index.test.ts | 2 +- .../integration_tests/lib/servers.ts | 4 +- 6 files changed, 43 insertions(+), 27 deletions(-) diff --git a/jest.config.integration.js b/jest.config.integration.js index 50767932a52d7..b6ecb4569b643 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -13,7 +13,6 @@ module.exports = { rootDir: '.', roots: ['/src', '/packages'], testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'], - testRunner: 'jasmine2', testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 7571184363d2e..dfc47098724cc 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -163,24 +163,24 @@ describe('KibanaRequest', () => { describe('events', () => { describe('aborted$', () => { - it('emits once and completes when request aborted', async (done) => { + it('emits once and completes when request aborted', async () => { expect.assertions(1); const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); const nextSpy = jest.fn(); - router.get({ path: '/', validate: false }, async (context, request, res) => { - request.events.aborted$.subscribe({ - next: nextSpy, - complete: () => { - expect(nextSpy).toHaveBeenCalledTimes(1); - done(); - }, - }); - // prevents the server to respond - await delay(30000); - return res.ok({ body: 'ok' }); + const done = new Promise((resolve) => { + router.get({ path: '/', validate: false }, async (context, request, res) => { + request.events.aborted$.subscribe({ + next: nextSpy, + complete: resolve, + }); + + // prevents the server to respond + await delay(30000); + return res.ok({ body: 'ok' }); + }); }); await server.start(); @@ -191,6 +191,8 @@ describe('KibanaRequest', () => { .end(); setTimeout(() => incomingRequest.abort(), 50); + await done; + expect(nextSpy).toHaveBeenCalledTimes(1); }); it('completes & does not emit when request handled', async () => { @@ -299,25 +301,24 @@ describe('KibanaRequest', () => { expect(completeSpy).toHaveBeenCalledTimes(1); }); - it('emits once and completes when response is aborted', async (done) => { + it('emits once and completes when response is aborted', async () => { expect.assertions(2); const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); const nextSpy = jest.fn(); - router.get({ path: '/', validate: false }, async (context, req, res) => { - req.events.completed$.subscribe({ - next: nextSpy, - complete: () => { - expect(nextSpy).toHaveBeenCalledTimes(1); - done(); - }, - }); + const done = new Promise((resolve) => { + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: resolve, + }); - expect(nextSpy).not.toHaveBeenCalled(); - await delay(30000); - return res.ok({ body: 'ok' }); + expect(nextSpy).not.toHaveBeenCalled(); + await delay(30000); + return res.ok({ body: 'ok' }); + }); }); await server.start(); @@ -327,6 +328,8 @@ describe('KibanaRequest', () => { // end required to send request .end(); setTimeout(() => incomingRequest.abort(), 50); + await done; + expect(nextSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index e82f2b222bb43..4f6c37e62179b 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -71,6 +71,12 @@ describe('migration v2', () => { }, ], }, + // reporting loads headless browser, that prevents nodejs process from exiting. + xpack: { + reporting: { + enabled: false, + }, + }, }, { oss, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index 0e51c886f7f30..5b7b0969a530a 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -70,6 +70,12 @@ describe('migration from 7.7.2-xpack with 100k objects', () => { }, ], }, + // reporting loads headless browser, that prevents nodejs process from exiting. + xpack: { + reporting: { + enabled: false, + }, + }, }, { oss, diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 6c7cdfa43cf57..61e55284a20b8 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -17,7 +17,7 @@ const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo. const savedObjectIndex = `.kibana_${kibanaVersion}_001`; describe('uiSettings/routes', function () { - jest.setTimeout(10000); + jest.setTimeout(120_000); beforeAll(startServers); /* eslint-disable jest/valid-describe */ diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index b18d9926649aa..96ba08a0728ab 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -75,8 +75,10 @@ export function getServices() { export async function stopServers() { services = null!; - if (servers) { + if (esServer) { await esServer.stop(); + } + if (kbn) { await kbn.stop(); } } From 7572d5150659c36632471f57c9b120d383ebc1a7 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 22 Jun 2021 15:54:59 -0400 Subject: [PATCH 072/118] [Cases] RBAC Bugs (#101325) (#102969) * Adding feature flag for auth * Hiding SOs and adding consumer field * First pass at adding security changes * Consumer as the app's plugin ID * Create addConsumerToSO migration helper * Fix mapping's SO consumer * Add test for CasesActions * Declare hidden types on SO client * Restructure integration tests * Init spaces_only integration tests * Implementing the cases security string * Adding security plugin tests for cases * Rough concept for authorization class * Adding comments * Fix merge * Get requiredPrivileges for classes * Check privillages * Ensure that all classes are available * Success if hasAllRequested is true * Failure if hasAllRequested is false * Adding schema updates for feature plugin * Seperate basic from trial * Enable SIR on integration tests * Starting the plumbing for authorization in plugin * Unit tests working * Move find route logic to case client * Create integration test helper functions * Adding auth to create call * Create getClassFilter helper * Add class attribute to find request * Create getFindAuthorizationFilter * Ensure savedObject is authorized in find method * Include fields for authorization * Combine authorization filter with cases & subcases filter * Fix isAuthorized flag * Fix merge issue * Create/delete spaces & users before and after tests * Add more user and roles * [Cases] Convert filters from strings to KueryNode (#95288) * [Cases] RBAC: Rename class to scope (#95535) * [Cases][RBAC] Rename scope to owner (#96035) * [Cases] RBAC: Create & Find integration tests (#95511) * [Cases] Cases client enchantment (#95923) * [Cases] Authorization and Client Audit Logger (#95477) * Starting audit logger * Finishing auth audit logger * Fixing tests and types * Adding audit event creator * Renaming class to scope * Adding audit logger messages to create and find * Adding comments and fixing import issue * Fixing type errors * Fixing tests and adding username to message * Addressing PR feedback * Removing unneccessary log and generating id * Fixing module issue and remove expect.anything * [Cases] Migrate sub cases routes to a client (#96461) * Adding sub cases client * Move sub case routes to case client * Throw when attempting to access the sub cases client * Fixing throw and removing user ans soclients * [Cases] RBAC: Migrate routes' unit tests to integration tests (#96374) Co-authored-by: Jonathan Buttner * [Cases] Move remaining HTTP functionality to client (#96507) * Moving deletes and find for attachments * Moving rest of comment apis * Migrating configuration routes to client * Finished moving routes, starting utils refactor * Refactoring utilites and fixing integration tests * Addressing PR feedback * Fixing mocks and types * Fixing integration tests * Renaming status_stats * Fixing test type errors * Adding plugins to kibana.json * Adding cases to required plugin * [Cases] Refactoring authorization (#97483) * Refactoring authorization * Wrapping auth calls in helper for try catch * Reverting name change * Hardcoding the saved object types * Switching ensure to owner array * [Cases] Add authorization to configuration & cases routes (#97228) * [Cases] Attachments RBAC (#97756) * Starting rbac for comments * Adding authorization to rest of comment apis * Starting the comment rbac tests * Fixing some of the rbac tests * Adding some integration tests * Starting patch tests * Working tests for comments * Working tests * Fixing some tests * Fixing type issues from pulling in master * Fixing connector tests that only work in trial license * Attempting to fix cypress * Mock return of array for configure * Fixing cypress test * Cleaning up * Addressing PR comments * Reducing operations * [Cases] Add RBAC to remaining Cases APIs (#98762) * Starting rbac for comments * Adding authorization to rest of comment apis * Starting the comment rbac tests * Fixing some of the rbac tests * Adding some integration tests * Starting patch tests * Working tests for comments * Working tests * Fixing some tests * Fixing type issues from pulling in master * Fixing connector tests that only work in trial license * Attempting to fix cypress * Mock return of array for configure * Fixing cypress test * Cleaning up * Working case update tests * Addressing PR comments * Reducing operations * Working rbac push case tests * Starting stats apis * Working status tests * User action tests and fixing migration errors * Fixing type errors * including error in message * Addressing pr feedback * Fixing some type errors * [Cases] Add space only tests (#99409) * Starting spaces tests * Finishing space only tests * Refactoring createCaseWithConnector * Fixing spelling * Addressing PR feedback and creating alert tests * Fixing mocks * [Cases] Add security only tests (#99679) * Starting spaces tests * Finishing space only tests * Refactoring createCaseWithConnector * Fixing spelling * Addressing PR feedback and creating alert tests * Fixing mocks * Starting security only tests * Adding remainder security only tests * Using helper objects * Fixing type error for null space * Renaming utility variables * Refactoring users and roles for security only tests * Adding sub feature * [Cases] Cleaning up the services and TODOs (#99723) * Cleaning up the service intialization * Fixing type errors * Adding comments for the api * Working test for cases client * Fix type error * Adding generated docs * Adding more docs and cleaning up types * Cleaning up readme * More clean up and links * Changing some file names * Renaming docs * Integration tests for cases privs and fixes (#100038) * [Cases] RBAC on UI (#99478) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * Fixing case ids by alert id route call * [Cases] Fixing UI feature permissions and adding UI tests (#100074) * Integration tests for cases privs and fixes * Fixing ui cases permissions and adding tests * Adding test for collection failure and fixing jest * Renaming variables * Fixing type error * Adding some comments * Validate cases features * Fix new schema * Adding owner param for the status stats * Fix get case status tests * Adjusting permissions text and fixing status * Address PR feedback * Adding top level feature back * Fixing feature privileges * Renaming * Removing uneeded else * Fixing tests and adding cases merge tests * [Cases][Security Solution] Basic license security solution API tests (#100925) * Cleaning up the fixture plugins * Adding basic feature test * renaming to unsecuredSavedObjectsClient (#101215) * [Cases] RBAC Refactoring audit logging (#100952) * Refactoring audit logging * Adding unit tests for authorization classes * Addressing feedback and adding util tests * return undefined on empty array * fixing eslint * conditional rendering the recently created cases * Remove unnecessary Array.from * Cleaning up overview page for permissions * Fixing log message for attachments * hiding add to cases button * Disable the Cases app from the global nav * Hide the add to cases button from detections * Fixing merge * Making progress on removing icons * Hding edit icons on detail view * Trying to get connector error msg tests working * Removing test * Disable error callouts * Fixing spacing and removing cases tab one no read * Adding read only badge * Cleaning up and adding badge * Wrapping in use effect * Default toasting permissions errors * Removing actions icon on comments * Addressing feedback * Fixing type Co-authored-by: Christos Nasikas Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christos Nasikas Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/add_comment/index.test.tsx | 26 ++- .../public/components/add_comment/index.tsx | 58 +++---- .../public/components/all_cases/header.tsx | 32 ++-- .../components/all_cases/nav_buttons.tsx | 5 +- .../public/components/all_cases/table.tsx | 26 +-- .../components/all_cases/translations.ts | 8 + .../public/components/callout/helpers.tsx | 11 -- .../public/components/callout/translations.ts | 9 - .../components/case_action_bar/actions.tsx | 5 +- .../components/case_action_bar/index.test.tsx | 1 + .../components/case_action_bar/index.tsx | 27 +-- .../public/components/case_view/index.tsx | 15 +- .../components/edit_connector/index.test.tsx | 55 +++++- .../components/edit_connector/index.tsx | 43 +++-- .../components/recent_cases/index.test.tsx | 1 + .../public/components/recent_cases/index.tsx | 3 + .../recent_cases/no_cases/index.test.tsx | 13 +- .../recent_cases/no_cases/index.tsx | 30 ++-- .../components/recent_cases/recent_cases.tsx | 4 +- .../components/recent_cases/translations.ts | 4 + .../cases/public/components/status/button.tsx | 9 +- .../public/components/status/status.test.tsx | 9 +- .../cases/public/components/status/status.tsx | 8 +- .../public/components/tag_list/index.test.tsx | 16 +- .../public/components/tag_list/index.tsx | 10 +- .../use_push_to_service/index.test.tsx | 153 +++++++++++++++++ .../components/use_push_to_service/index.tsx | 11 +- .../components/user_action_tree/index.tsx | 37 +++-- .../user_action_content_toolbar.test.tsx | 9 +- .../user_action_content_toolbar.tsx | 29 ++-- .../user_action_property_actions.tsx | 6 +- .../containers/configure/translations.ts | 8 + .../containers/configure/use_connectors.tsx | 46 ++++-- x-pack/plugins/cases/public/mocks.ts | 21 +++ .../server/authorization/audit_logger.test.ts | 8 +- x-pack/plugins/cases/server/plugin.ts | 2 +- .../components/app/cases/callout/helpers.tsx | 11 -- .../app/cases/callout/translations.ts | 15 -- .../components/app/cases/translations.ts | 14 ++ .../public/hooks/use_readonly_header.tsx | 40 +++++ .../public/pages/cases/all_cases.tsx | 25 +-- .../public/pages/cases/case_details.tsx | 30 ++-- .../public/pages/cases/configure_cases.tsx | 12 +- .../public/pages/cases/create_case.tsx | 12 +- .../security_solution/common/constants.ts | 3 + .../detection_alerts/attach_to_case.spec.ts | 2 +- .../cases/components/callout/helpers.tsx | 11 -- .../cases/components/callout/translations.ts | 15 -- .../add_to_case_action.test.tsx | 6 +- .../timeline_actions/add_to_case_action.tsx | 28 ++-- .../public/cases/pages/case.tsx | 7 - .../public/cases/pages/case_details.tsx | 18 +- .../public/cases/pages/configure_cases.tsx | 13 +- .../public/cases/pages/create_case.tsx | 16 +- .../public/cases/pages/index.test.tsx | 91 ++++++++++ .../public/cases/pages/index.tsx | 77 ++++++--- .../public/cases/pages/translations.ts | 21 +++ .../components/header_global/index.test.tsx | 51 ++++++ .../common/components/header_global/index.tsx | 23 ++- .../components/recent_cases/index.tsx | 5 +- .../components/sidebar/sidebar.test.tsx | 72 ++++++++ .../overview/components/sidebar/sidebar.tsx | 16 +- .../components/flyout/header/index.test.tsx | 156 +++++++++++------- .../components/flyout/header/index.tsx | 12 +- .../security_solution/server/plugin.ts | 20 ++- .../observability_security.ts | 10 +- .../page_objects/observability_page.ts | 16 +- 67 files changed, 1114 insertions(+), 492 deletions(-) create mode 100644 x-pack/plugins/cases/public/mocks.ts create mode 100644 x-pack/plugins/observability/public/hooks/use_readonly_header.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/pages/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 19c303840fc1a..078db1e6dbe6d 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -14,7 +14,7 @@ import { TestProviders } from '../../common/mock'; import { CommentRequest, CommentType, SECURITY_SOLUTION_OWNER } from '../../../common'; import { usePostComment } from '../../containers/use_post_comment'; -import { AddComment, AddCommentRefObject } from '.'; +import { AddComment, AddCommentProps, AddCommentRefObject } from '.'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { timelineIntegrationMock } from '../__mock__/timeline'; @@ -25,10 +25,9 @@ const onCommentSaving = jest.fn(); const onCommentPosted = jest.fn(); const postComment = jest.fn(); -const addCommentProps = { +const addCommentProps: AddCommentProps = { caseId: '1234', - disabled: false, - insertQuote: null, + userCanCrud: true, onCommentSaving, onCommentPosted, showLoading: false, @@ -94,11 +93,11 @@ describe('AddComment ', () => { ).toBeTruthy(); }); - it('should disable submit button when disabled prop passed', () => { + it('should disable submit button when isLoading is true', () => { usePostCommentMock.mockImplementation(() => ({ ...defaultPostComment, isLoading: true })); const wrapper = mount( - + ); @@ -107,12 +106,23 @@ describe('AddComment ', () => { ).toBeTruthy(); }); + it('should hide the component when the user does not have crud permissions', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostComment, isLoading: true })); + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeFalsy(); + }); + it('should insert a quote', async () => { const sampleQuote = 'what a cool quote'; const ref = React.createRef(); const wrapper = mount( - + ); @@ -143,7 +153,7 @@ describe('AddComment ', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 04104f0b9471d..6604f3d2b8bc8 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -33,9 +33,9 @@ export interface AddCommentRefObject { addQuote: (quote: string) => void; } -interface AddCommentProps { +export interface AddCommentProps { caseId: string; - disabled?: boolean; + userCanCrud?: boolean; onCommentSaving?: () => void; onCommentPosted: (newCase: Case) => void; showLoading?: boolean; @@ -45,7 +45,7 @@ interface AddCommentProps { export const AddComment = React.memo( forwardRef( ( - { caseId, disabled, onCommentPosted, onCommentSaving, showLoading = true, subCaseId }, + { caseId, userCanCrud, onCommentPosted, onCommentSaving, showLoading = true, subCaseId }, ref ) => { const owner = useOwnerContext(); @@ -91,31 +91,33 @@ export const AddComment = React.memo( return ( {isLoading && showLoading && } - - - {i18n.ADD_COMMENT} - - ), - }} - /> - - + {userCanCrud && ( +
+ + {i18n.ADD_COMMENT} + + ), + }} + /> + + + )}
); } diff --git a/x-pack/plugins/cases/public/components/all_cases/header.tsx b/x-pack/plugins/cases/public/components/all_cases/header.tsx index 7452fe7e44b3c..73dcc18b97108 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.tsx @@ -52,17 +52,27 @@ export const CasesTableHeader: FunctionComponent = ({ wrap={true} data-test-subj="all-cases-header" > - - - - - - + {userCanCrud ? ( + <> + + + + + + + + + ) : ( + // doesn't include the horizontal bar that divides the buttons and other padding since we don't have any buttons + // to the right + + + + )} ); diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx index e29551f43c2bd..b8755d03e0b00 100644 --- a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx @@ -17,7 +17,6 @@ interface OwnProps { actionsErrors: ErrorMessage[]; configureCasesNavigation: CasesNavigation; createCaseNavigation: CasesNavigation; - userCanCrud: boolean; } type Props = OwnProps; @@ -26,14 +25,13 @@ export const NavButtons: FunctionComponent = ({ actionsErrors, configureCasesNavigation, createCaseNavigation, - userCanCrud, }) => ( } titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} @@ -41,7 +39,6 @@ export const NavButtons: FunctionComponent = ({ = ({ {i18n.NO_CASES}} titleSize="xs" - body={i18n.NO_CASES_BODY} + body={userCanCrud ? i18n.NO_CASES_BODY : i18n.NO_CASES_BODY_READ_ONLY} actions={ - - {i18n.ADD_NEW_CASE} - + userCanCrud && ( + + {i18n.ADD_NEW_CASE} + + ) } /> } diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index 0f535b771ec8a..8da90f32fabdf 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -12,11 +12,19 @@ export * from '../../common/translations'; export const NO_CASES = i18n.translate('xpack.cases.caseTable.noCases.title', { defaultMessage: 'No Cases', }); + export const NO_CASES_BODY = i18n.translate('xpack.cases.caseTable.noCases.body', { defaultMessage: 'There are no cases to display. Please create a new case or change your filter settings above.', }); +export const NO_CASES_BODY_READ_ONLY = i18n.translate( + 'xpack.cases.caseTable.noCases.readonly.body', + { + defaultMessage: 'There are no cases to display. Please change your filter settings above.', + } +); + export const ADD_NEW_CASE = i18n.translate('xpack.cases.caseTable.addNewCase', { defaultMessage: 'Add New Case', }); diff --git a/x-pack/plugins/cases/public/components/callout/helpers.tsx b/x-pack/plugins/cases/public/components/callout/helpers.tsx index 29b17cd426c58..fdd49ad17168d 100644 --- a/x-pack/plugins/cases/public/components/callout/helpers.tsx +++ b/x-pack/plugins/cases/public/components/callout/helpers.tsx @@ -5,18 +5,7 @@ * 2.0. */ -import React from 'react'; import md5 from 'md5'; -import * as i18n from './translations'; -import { ErrorMessage } from './types'; - -export const permissionsReadOnlyErrorMessage: ErrorMessage = { - id: 'read-only-privileges-error', - title: i18n.READ_ONLY_FEATURE_TITLE, - description: <>{i18n.READ_ONLY_FEATURE_MSG}, - errorType: 'warning', -}; - export const createCalloutId = (ids: string[], delimiter: string = '|'): string => md5(ids.join(delimiter)); diff --git a/x-pack/plugins/cases/public/components/callout/translations.ts b/x-pack/plugins/cases/public/components/callout/translations.ts index dca622e60c863..8b0ad31dba88e 100644 --- a/x-pack/plugins/cases/public/components/callout/translations.ts +++ b/x-pack/plugins/cases/public/components/callout/translations.ts @@ -7,15 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_FEATURE_TITLE = i18n.translate('xpack.cases.readOnlyFeatureTitle', { - defaultMessage: 'You cannot open new or update existing cases', -}); - -export const READ_ONLY_FEATURE_MSG = i18n.translate('xpack.cases.readOnlyFeatureDescription', { - defaultMessage: - 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', -}); - export const DISMISS_CALLOUT = i18n.translate('xpack.cases.dismissErrorsPushServiceCallOutTitle', { defaultMessage: 'Dismiss', }); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx index c2578dc3debdb..6816575d649f7 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx @@ -19,14 +19,12 @@ interface CaseViewActions { allCasesNavigation: CasesNavigation; caseData: Case; currentExternalIncident: CaseService | null; - disabled?: boolean; } const ActionsComponent: React.FC = ({ allCasesNavigation, caseData, currentExternalIncident, - disabled = false, }) => { // Delete case const { @@ -39,7 +37,6 @@ const ActionsComponent: React.FC = ({ const propertyActions = useMemo( () => [ { - disabled, iconType: 'trash', label: i18n.DELETE_CASE(), onClick: handleToggleModal, @@ -54,7 +51,7 @@ const ActionsComponent: React.FC = ({ ] : []), ], - [disabled, handleToggleModal, currentExternalIncident] + [handleToggleModal, currentExternalIncident] ); if (isDeleted) { diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index 724d35b20df53..3040b0fe47a47 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -26,6 +26,7 @@ describe('CaseActionBar', () => { onRefresh, onUpdateField, currentExternalIncident: null, + userCanCrud: true, }; beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index d8e012b072106..3448d112dadd1 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -40,7 +40,7 @@ interface CaseActionBarProps { allCasesNavigation: CasesNavigation; caseData: Case; currentExternalIncident: CaseService | null; - disabled?: boolean; + userCanCrud: boolean; disableAlerting: boolean; isLoading: boolean; onRefresh: () => void; @@ -50,8 +50,8 @@ const CaseActionBarComponent: React.FC = ({ allCasesNavigation, caseData, currentExternalIncident, - disabled = false, disableAlerting, + userCanCrud, isLoading, onRefresh, onUpdateField, @@ -87,7 +87,7 @@ const CaseActionBarComponent: React.FC = ({ @@ -108,7 +108,7 @@ const CaseActionBarComponent: React.FC = ({ - {!disableAlerting && ( + {userCanCrud && !disableAlerting && ( @@ -122,7 +122,7 @@ const CaseActionBarComponent: React.FC = ({ @@ -134,14 +134,15 @@ const CaseActionBarComponent: React.FC = ({ {i18n.CASE_REFRESH} - - - + {userCanCrud && ( + + + + )} diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index df57e49073a60..05f1c6727b168 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -230,7 +230,9 @@ export const CaseComponent = React.memo( [updateCase, fetchCaseUserActions, caseId, subCaseId] ); - const { loading: isLoadingConnectors, connectors } = useConnectors(); + const { loading: isLoadingConnectors, connectors, permissionsError } = useConnectors({ + toastPermissionsErrors: false, + }); const [connectorName, isValidConnector] = useMemo(() => { const connector = connectors.find((c) => c.id === caseData.connector.id); @@ -363,7 +365,7 @@ export const CaseComponent = React.memo( allCasesNavigation={allCasesNavigation} caseData={caseData} currentExternalIncident={currentExternalIncident} - disabled={!userCanCrud} + userCanCrud={userCanCrud} disableAlerting={ruleDetailsNavigation == null} isLoading={isLoading && (updateKey === 'status' || updateKey === 'settings')} onRefresh={handleRefresh} @@ -406,7 +408,7 @@ export const CaseComponent = React.memo( useFetchAlertData={useFetchAlertData} userCanCrud={userCanCrud} /> - {(caseData.type !== CaseType.collection || hasDataToPush) && ( + {(caseData.type !== CaseType.collection || hasDataToPush) && userCanCrud && ( <> ( @@ -450,16 +451,15 @@ export const CaseComponent = React.memo( /> ( onSubmit={onSubmitConnector} selectedConnector={caseData.connector.id} userActions={caseUserActions} + permissionsError={permissionsError} />
diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index 1385e8e8664c3..33efb7e447583 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; -import { EditConnector } from './index'; +import { EditConnector, EditConnectorProps } from './index'; import { getFormMock, useFormMock } from '../__mock__/form'; import { TestProviders } from '../../common/mock'; import { connectorsMock } from '../../containers/configure/mock'; @@ -21,9 +21,9 @@ jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; const onSubmit = jest.fn(); -const defaultProps = { +const defaultProps: EditConnectorProps = { connectors: connectorsMock, - disabled: false, + userCanCrud: true, isLoading: false, onSubmit, selectedConnector: 'none', @@ -144,4 +144,53 @@ describe('EditConnector ', () => { expect(wrapper.find(`[data-test-subj="connector-loading"]`).last().exists()).toBeTruthy() ); }); + + it('does not allow the connector to be edited when the user does not have write permissions', async () => { + const props = { ...defaultProps, userCanCrud: false }; + const wrapper = mount( + + + + ); + await waitFor(() => + expect(wrapper.find(`[data-test-subj="connector-edit"]`).exists()).toBeFalsy() + ); + }); + + it('displays the permissions error message when one is provided', async () => { + const props = { ...defaultProps, permissionsError: 'error message' }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="edit-connector-permissions-error-msg"]`).exists() + ).toBeTruthy(); + + expect( + wrapper.find(`[data-test-subj="edit-connector-no-connectors-msg"]`).exists() + ).toBeFalsy(); + }); + }); + + it('displays the default none connector message', async () => { + const props = { ...defaultProps }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="edit-connector-permissions-error-msg"]`).exists() + ).toBeFalsy(); + expect( + wrapper.find(`[data-test-subj="edit-connector-no-connectors-msg"]`).exists() + ).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index ad6b5a5e7cddf..570f6e34d2528 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -30,7 +30,7 @@ import { schema } from './schema'; import { getConnectorFieldsFromUserActions } from './helpers'; import * as i18n from './translations'; -interface EditConnectorProps { +export interface EditConnectorProps { caseFields: ConnectorTypeFields['fields']; connectors: ActionConnector[]; isLoading: boolean; @@ -42,8 +42,9 @@ interface EditConnectorProps { ) => void; selectedConnector: string; userActions: CaseUserActions[]; - disabled?: boolean; + userCanCrud?: boolean; hideConnectorServiceNowSir?: boolean; + permissionsError?: string; } const MyFlexGroup = styled(EuiFlexGroup)` @@ -104,12 +105,13 @@ export const EditConnector = React.memo( ({ caseFields, connectors, - disabled = false, + userCanCrud = true, hideConnectorServiceNowSir = false, isLoading, onSubmit, selectedConnector, userActions, + permissionsError, }: EditConnectorProps) => { const { form } = useForm({ defaultValue: { connectorId: selectedConnector }, @@ -203,6 +205,18 @@ export const EditConnector = React.memo( }); }, [dispatch]); + /** + * if this evaluates to true it means that the connector was likely deleted because the case connector was set to something + * other than none but we don't find it in the list of connectors returned from the actions plugin + */ + const connectorFromCaseMissing = currentConnector == null && selectedConnector !== 'none'; + + /** + * True if the chosen connector from the form was the "none" connector or no connector was in the case. The + * currentConnector will be null initially and after the form initializes if the case connector is "none" + */ + const connectorUndefinedOrNone = currentConnector == null || currentConnector?.id === 'none'; + return ( @@ -210,11 +224,10 @@ export const EditConnector = React.memo(

{i18n.CONNECTORS}

{isLoading && } - {!isLoading && !editConnector && ( + {!isLoading && !editConnector && userCanCrud && ( - {(currentConnector == null || currentConnector?.id === 'none') && // Connector is none or not defined. - !(currentConnector === null && selectedConnector !== 'none') && // Connector has not been deleted. - !editConnector && ( - + {!editConnector && permissionsError ? ( + + {permissionsError} + + ) : ( + // if we're not editing the connectors and the connector specified in the case was found and the connector + // is undefined or explicitly set to none + !editConnector && + !connectorFromCaseMissing && + connectorUndefinedOrNone && ( + {i18n.NO_CONNECTOR} - )} + ) + )} ; createCaseNavigation: CasesNavigation; + hasWritePermissions: boolean; maxCasesToShow: number; } @@ -29,6 +30,7 @@ const RecentCasesComponent = ({ caseDetailsNavigation, createCaseNavigation, maxCasesToShow, + hasWritePermissions, }: Omit) => { const currentUser = useCurrentUser(); const [recentCasesFilterBy, setRecentCasesFilterBy] = useState( @@ -77,6 +79,7 @@ const RecentCasesComponent = ({ createCaseNavigation={createCaseNavigation} filterOptions={recentCasesFilterOptions} maxCasesToShow={maxCasesToShow} + hasWritePermissions={hasWritePermissions} /> diff --git a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx index 0295632cc137a..10fef0bb82df9 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx @@ -16,11 +16,22 @@ describe('RecentCases', () => { const createCaseHref = '/create'; const wrapper = mount( - + ); expect(wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().prop('href')).toEqual( createCaseHref ); }); + + it('displays a message without a link to create a case when the user does not have write permissions', () => { + const createCaseHref = '/create'; + const wrapper = mount( + + + + ); + expect(wrapper.find(`[data-test-subj="no-cases-create-case"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="no-cases-readonly"]`).exists()).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx index df0efcec4552c..a5b90943a219a 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx @@ -10,16 +10,26 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; import * as i18n from '../translations'; -const NoCasesComponent = ({ createCaseHref }: { createCaseHref: string }) => ( - <> - {i18n.NO_CASES} - {` ${i18n.START_A_NEW_CASE}`} - {'!'} - -); +const NoCasesComponent = ({ + createCaseHref, + hasWritePermissions, +}: { + createCaseHref: string; + hasWritePermissions: boolean; +}) => { + return hasWritePermissions ? ( + <> + {i18n.NO_CASES} + {` ${i18n.START_A_NEW_CASE}`} + {'!'} + + ) : ( + {i18n.NO_CASES_READ_ONLY} + ); +}; NoCasesComponent.displayName = 'NoCasesComponent'; diff --git a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx index 5b4313530e490..bfe44dda6c6ef 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx @@ -31,6 +31,7 @@ export interface RecentCasesProps { caseDetailsNavigation: CasesNavigation; createCaseNavigation: CasesNavigation; maxCasesToShow: number; + hasWritePermissions: boolean; } const usePrevious = (value: Partial) => { @@ -45,6 +46,7 @@ export const RecentCasesComp = ({ createCaseNavigation, filterOptions, maxCasesToShow, + hasWritePermissions, }: RecentCasesProps) => { const previousFilterOptions = usePrevious(filterOptions); const { data, loading, setFilters } = useGetCases({ @@ -65,7 +67,7 @@ export const RecentCasesComp = ({ return isLoadingCases ? ( ) : !isLoadingCases && data.cases.length === 0 ? ( - + ) : ( <> {data.cases.map((c, i) => ( diff --git a/x-pack/plugins/cases/public/components/recent_cases/translations.ts b/x-pack/plugins/cases/public/components/recent_cases/translations.ts index c8f6c349d8f72..653bda4be2ebc 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/recent_cases/translations.ts @@ -22,6 +22,10 @@ export const NO_CASES = i18n.translate('xpack.cases.recentCases.noCasesMessage', defaultMessage: 'No cases have been created yet. Put your detective hat on and', }); +export const NO_CASES_READ_ONLY = i18n.translate('xpack.cases.recentCases.noCasesMessageReadOnly', { + defaultMessage: 'No cases have been created yet.', +}); + export const RECENT_CASES = i18n.translate('xpack.cases.recentCases.recentCasesSidebarTitle', { defaultMessage: 'Recent cases', }); diff --git a/x-pack/plugins/cases/public/components/status/button.tsx b/x-pack/plugins/cases/public/components/status/button.tsx index 623afeb43c596..675d83c759bc7 100644 --- a/x-pack/plugins/cases/public/components/status/button.tsx +++ b/x-pack/plugins/cases/public/components/status/button.tsx @@ -13,7 +13,6 @@ import { statuses } from './config'; interface Props { status: CaseStatuses; - disabled: boolean; isLoading: boolean; onStatusChanged: (status: CaseStatuses) => void; } @@ -21,12 +20,7 @@ interface Props { // Rotate over the statuses. open -> in-progress -> closes -> open... const getNextItem = (item: number) => (item + 1) % caseStatuses.length; -const StatusActionButtonComponent: React.FC = ({ - status, - onStatusChanged, - disabled, - isLoading, -}) => { +const StatusActionButtonComponent: React.FC = ({ status, onStatusChanged, isLoading }) => { const indexOfCurrentStatus = useMemo( () => caseStatuses.findIndex((caseStatus) => caseStatus === status), [status] @@ -41,7 +35,6 @@ const StatusActionButtonComponent: React.FC = ({ diff --git a/x-pack/plugins/cases/public/components/status/status.test.tsx b/x-pack/plugins/cases/public/components/status/status.test.tsx index 4d13e57fbdee7..a685256741c43 100644 --- a/x-pack/plugins/cases/public/components/status/status.test.tsx +++ b/x-pack/plugins/cases/public/components/status/status.test.tsx @@ -42,17 +42,14 @@ describe('Stats', () => { ).toBe(false); }); - it('it renders with the pop over disabled when initialized disabled', async () => { + it('renders without the arrow and is not clickable when initialized disabled', async () => { const wrapper = mount( ); expect( - wrapper - .find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`) - .first() - .prop('disabled') - ).toBe(true); + wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).exists() + ).toBeFalsy(); }); it('it calls onClick when pressing the badge', async () => { diff --git a/x-pack/plugins/cases/public/components/status/status.tsx b/x-pack/plugins/cases/public/components/status/status.tsx index 3b832ce155400..3c186313a151a 100644 --- a/x-pack/plugins/cases/public/components/status/status.tsx +++ b/x-pack/plugins/cases/public/components/status/status.tsx @@ -29,18 +29,18 @@ const StatusComponent: React.FC = ({ const props = useMemo( () => ({ color: type === StatusAll ? allCaseStatus[StatusAll].color : statuses[type].color, - ...(withArrow ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}), + // if we are disabled, don't show the arrow and don't allow the user to click + ...(withArrow && !disabled ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}), + ...(!disabled ? { iconOnClick: onClick } : { iconOnClick: noop }), }), - [withArrow, type] + [disabled, onClick, withArrow, type] ); return ( {type === StatusAll ? allCaseStatus[StatusAll].label : statuses[type].label} diff --git a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx index b3fbcd30d4e97..2ced7502b3c3f 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx @@ -8,13 +8,12 @@ import React from 'react'; import { mount } from 'enzyme'; -import { TagList } from '.'; +import { TagList, TagListProps } from '.'; import { getFormMock } from '../__mock__/form'; import { TestProviders } from '../../common/mock'; import { waitFor } from '@testing-library/react'; import { useForm } from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; import { useGetTags } from '../../containers/use_get_tags'; -import { SECURITY_SOLUTION_OWNER } from '../../../common'; jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'); jest.mock('../../containers/use_get_tags'); @@ -33,12 +32,11 @@ jest.mock('@elastic/eui', () => { }; }); const onSubmit = jest.fn(); -const defaultProps = { - disabled: false, +const defaultProps: TagListProps = { + userCanCrud: true, isLoading: false, onSubmit, tags: [], - owner: [SECURITY_SOLUTION_OWNER], }; describe('TagList ', () => { @@ -110,15 +108,13 @@ describe('TagList ', () => { expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy(); }); - it('Renders disabled button', () => { - const props = { ...defaultProps, disabled: true }; + it('does not render when the user does not have write permissions', () => { + const props = { ...defaultProps, userCanCrud: false }; const wrapper = mount( ); - expect( - wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().prop('disabled') - ).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="tag-list-edit"]`).exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/cases/public/components/tag_list/index.tsx b/x-pack/plugins/cases/public/components/tag_list/index.tsx index f260593369679..4e8946a6589a3 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/index.tsx @@ -27,12 +27,11 @@ import { Tags } from './tags'; const CommonUseField = getUseField({ component: Field }); -interface TagListProps { - disabled?: boolean; +export interface TagListProps { + userCanCrud?: boolean; isLoading: boolean; onSubmit: (a: string[]) => void; tags: string[]; - owner: string[]; } const MyFlexGroup = styled(EuiFlexGroup)` @@ -45,7 +44,7 @@ const MyFlexGroup = styled(EuiFlexGroup)` `; export const TagList = React.memo( - ({ disabled = false, isLoading, onSubmit, tags, owner }: TagListProps) => { + ({ userCanCrud = true, isLoading, onSubmit, tags }: TagListProps) => { const initialState = { tags }; const { form } = useForm({ defaultValue: initialState, @@ -86,11 +85,10 @@ export const TagList = React.memo(

{i18n.TAGS}

{isLoading && } - {!isLoading && ( + {!isLoading && userCanCrud && ( { expect(errorsMsg[0].id).toEqual('closed-case-push-error'); }); }); + + describe('user does not have write permissions', () => { + const noWriteProps = { ...defaultArgs, userCanCrud: false }; + + it('does not display a message when user does not have a premium license', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInLicense: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => usePushToService(noWriteProps), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + + it('does not display a message when user does not have case enabled in config', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInConfig: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => usePushToService(noWriteProps), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + + it('does not display a message when user does not have any connector configured', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...noWriteProps, + connectors: [], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + + it('does not display a message when user does have a connector but is configured to none', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...noWriteProps, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + + it('does not display a message when connector is deleted', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...noWriteProps, + connector: { + id: 'not-exist', + name: 'not-exist', + type: ConnectorTypes.none, + fields: null, + }, + isValidConnector: false, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + + it('does not display a message when connector is deleted with empty connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...noWriteProps, + connectors: [], + connector: { + id: 'not-exist', + name: 'not-exist', + type: ConnectorTypes.none, + fields: null, + }, + isValidConnector: false, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + + it('does not display a message when case is closed', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...noWriteProps, + caseStatus: CaseStatuses.closed, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx index 00b88d372584b..6f711150b7744 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx @@ -67,9 +67,17 @@ export const usePushToService = ({ const errorsMsg = useMemo(() => { let errors: ErrorMessage[] = []; + + // these message require that the user do some sort of write action as a result of the message, readonly users won't + // be able to perform such an action so let's not display the error to the user in that situation + if (!userCanCrud) { + return errors; + } + if (actionLicense != null && !actionLicense.enabledInLicense) { errors = [...errors, getLicenseError()]; } + if (connectors.length === 0 && connector.id === 'none' && !loadingLicense) { errors = [ ...errors, @@ -136,12 +144,13 @@ export const usePushToService = ({ }, ]; } + if (actionLicense != null && !actionLicense.enabledInConfig) { errors = [...errors, getKibanaConfigError()]; } return errors; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionLicense, caseStatus, connectors.length, connector, loadingLicense]); + }, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, userCanCrud]); const pushToServiceButton = useMemo( () => ( diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index f9bd941547078..c7cc71da92947 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -241,7 +241,7 @@ export const UserActionTree = React.memo( () => ( ), }), @@ -363,10 +363,10 @@ export const UserActionTree = React.memo( id={comment.id} editLabel={i18n.EDIT_COMMENT} quoteLabel={i18n.QUOTE} - disabled={!userCanCrud} isLoading={isLoadingIds.includes(comment.id)} onEdit={handleManageMarkdownEditId.bind(null, comment.id)} onQuote={handleManageQuote.bind(null, comment.comment)} + userCanCrud={userCanCrud} /> ), }, @@ -571,19 +571,24 @@ export const UserActionTree = React.memo( ] ); - const bottomActions = [ - { - username: ( - - ), - 'data-test-subj': 'add-comment', - timelineIcon: ( - - ), - className: 'isEdit', - children: MarkdownNewComment, - }, - ]; + const bottomActions = userCanCrud + ? [ + { + username: ( + + ), + 'data-test-subj': 'add-comment', + timelineIcon: ( + + ), + className: 'isEdit', + children: MarkdownNewComment, + }, + ] + : []; const comments = [...userActions, ...bottomActions]; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx index a5244e14ad243..155e9e2323e64 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx @@ -7,7 +7,10 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { UserActionContentToolbar } from './user_action_content_toolbar'; +import { + UserActionContentToolbar, + UserActionContentToolbarProps, +} from './user_action_content_toolbar'; jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -28,12 +31,12 @@ jest.mock('../../common/lib/kibana', () => ({ }), })); -const props = { +const props: UserActionContentToolbarProps = { getCaseDetailHrefWithCommentId: jest.fn().mockReturnValue('case-detail-url-with-comment-id-1'), id: '1', editLabel: 'edit', quoteLabel: 'quote', - disabled: false, + userCanCrud: true, isLoading: false, onEdit: jest.fn(), onQuote: jest.fn(), diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx index 7adaffce22c54..5fa12b8cfa434 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx @@ -11,15 +11,15 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { UserActionCopyLink } from './user_action_copy_link'; import { UserActionPropertyActions } from './user_action_property_actions'; -interface UserActionContentToolbarProps { +export interface UserActionContentToolbarProps { id: string; getCaseDetailHrefWithCommentId: (commentId: string) => string; editLabel: string; quoteLabel: string; - disabled: boolean; isLoading: boolean; onEdit: (id: string) => void; onQuote: (id: string) => void; + userCanCrud: boolean; } const UserActionContentToolbarComponent = ({ @@ -27,26 +27,27 @@ const UserActionContentToolbarComponent = ({ getCaseDetailHrefWithCommentId, editLabel, quoteLabel, - disabled, isLoading, onEdit, onQuote, + userCanCrud, }: UserActionContentToolbarProps) => ( - - - + {userCanCrud && ( + + + + )} ); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx index 44b5baf3246cc..ebc83de1ef36a 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx @@ -14,7 +14,6 @@ interface UserActionPropertyActionsProps { id: string; editLabel: string; quoteLabel: string; - disabled: boolean; isLoading: boolean; onEdit: (id: string) => void; onQuote: (id: string) => void; @@ -24,7 +23,6 @@ const UserActionPropertyActionsComponent = ({ id, editLabel, quoteLabel, - disabled, isLoading, onEdit, onQuote, @@ -35,19 +33,17 @@ const UserActionPropertyActionsComponent = ({ const propertyActions = useMemo( () => [ { - disabled, iconType: 'pencil', label: editLabel, onClick: onEditClick, }, { - disabled, iconType: 'quote', label: quoteLabel, onClick: onQuoteClick, }, ], - [disabled, editLabel, quoteLabel, onEditClick, onQuoteClick] + [editLabel, quoteLabel, onEditClick, onQuoteClick] ); return ( <> diff --git a/x-pack/plugins/cases/public/containers/configure/translations.ts b/x-pack/plugins/cases/public/containers/configure/translations.ts index e77b9f57c8f4c..01900b8850c19 100644 --- a/x-pack/plugins/cases/public/containers/configure/translations.ts +++ b/x-pack/plugins/cases/public/containers/configure/translations.ts @@ -12,3 +12,11 @@ export * from '../translations'; export const SUCCESS_CONFIGURE = i18n.translate('xpack.cases.configure.successSaveToast', { defaultMessage: 'Saved external connection settings', }); + +export const READ_PERMISSIONS_ERROR_MSG = i18n.translate( + 'xpack.cases.configure.readPermissionsErrorDescription', + { + defaultMessage: + 'You do not have permissions to view connectors. If you would like to view the connectors associated with this case, contact your Kibana administrator.', + } +); diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx index 3b91c77d0235a..e350146c650ce 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx @@ -7,26 +7,40 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import * as i18n from '../translations'; import { fetchConnectors } from './api'; import { ActionConnector } from './types'; import { useToasts } from '../../common/lib/kibana'; +import * as i18n from './translations'; + +interface ConnectorsState { + loading: boolean; + connectors: ActionConnector[]; + permissionsError?: string; +} export interface UseConnectorsResponse { loading: boolean; connectors: ActionConnector[]; refetchConnectors: () => void; + permissionsError?: string; } -export const useConnectors = (): UseConnectorsResponse => { +/** + * Retrieves the configured case connectors + * + * @param toastPermissionsErrors boolean controlling whether 403 and 401 errors should be displayed in a toast error + */ +export const useConnectors = ({ + toastPermissionsErrors = true, +}: { + toastPermissionsErrors?: boolean; +} = {}): UseConnectorsResponse => { const toasts = useToasts(); - const [state, setState] = useState<{ - loading: boolean; - connectors: ActionConnector[]; - }>({ + const [state, setState] = useState({ loading: true, connectors: [], }); + const isCancelledRef = useRef(false); const abortCtrlRef = useRef(new AbortController()); @@ -49,15 +63,26 @@ export const useConnectors = (): UseConnectorsResponse => { } } catch (error) { if (!isCancelledRef.current) { + let permissionsError: string | undefined; if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); + // if the error was related to permissions then let's return a boilerplate error message describing the problem + if (error.body?.statusCode === 403 || error.body?.statusCode === 401) { + permissionsError = i18n.READ_PERMISSIONS_ERROR_MSG; + } + + // if the error was not permissions related then toast it + // if it was permissions related (permissionsError was defined) and the caller wants to toast, then create a toast + if (permissionsError === undefined || toastPermissionsErrors) { + toasts.addError( + error.body && error.body.message ? new Error(error.body.message) : error, + { title: i18n.ERROR_TITLE } + ); + } } setState({ loading: false, connectors: [], + permissionsError, }); } } @@ -77,5 +102,6 @@ export const useConnectors = (): UseConnectorsResponse => { loading: state.loading, connectors: state.connectors, refetchConnectors, + permissionsError: state.permissionsError, }; }; diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts new file mode 100644 index 0000000000000..c543baa477475 --- /dev/null +++ b/x-pack/plugins/cases/public/mocks.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasesUiStart } from './types'; + +const createStartContract = (): jest.Mocked => ({ + getAllCases: jest.fn(), + getAllCasesSelectorModal: jest.fn(), + getCaseView: jest.fn(), + getConfigureCases: jest.fn(), + getCreateCase: jest.fn(), + getRecentCases: jest.fn(), +}); + +export const casesPluginMock = { + createStartContract, +}; diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts index d54b5164b10b9..48c6e9ebcd07a 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts @@ -143,7 +143,7 @@ describe('audit_logger', () => { // for reference: https://github.com/facebook/jest/issues/9409#issuecomment-629272237 // This loops through all operation keys - it.each(Array.from(Object.keys(Operations)))( + it.each(Object.keys(Operations))( `creates the correct audit event for operation: "%s" without an error or entity`, (operationKey) => { // forcing the cast here because using a string throws a type error @@ -156,7 +156,7 @@ describe('audit_logger', () => { ); // This loops through all operation keys - it.each(Array.from(Object.keys(Operations)))( + it.each(Object.keys(Operations))( `creates the correct audit event for operation: "%s" with an error but no entity`, (operationKey) => { // forcing the cast here because using a string throws a type error @@ -170,7 +170,7 @@ describe('audit_logger', () => { ); // This loops through all operation keys - it.each(Array.from(Object.keys(Operations)))( + it.each(Object.keys(Operations))( `creates the correct audit event for operation: "%s" with an error and entity`, (operationKey) => { // forcing the cast here because using a string throws a type error @@ -188,7 +188,7 @@ describe('audit_logger', () => { ); // This loops through all operation keys - it.each(Array.from(Object.keys(Operations)))( + it.each(Object.keys(Operations))( `creates the correct audit event for operation: "%s" without an error but with an entity`, (operationKey) => { // forcing the cast here because using a string throws a type error diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 28b9cf9e4e032..b1e2f61a595ee 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -72,7 +72,7 @@ export class CasePlugin { this.clientFactory = new CasesClientFactory(this.log); } - public async setup(core: CoreSetup, plugins: PluginsSetup) { + public setup(core: CoreSetup, plugins: PluginsSetup) { const config = createConfig(this.initializerContext); if (!config.enabled) { diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx index 29b17cd426c58..fdd49ad17168d 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx @@ -5,18 +5,7 @@ * 2.0. */ -import React from 'react'; import md5 from 'md5'; -import * as i18n from './translations'; -import { ErrorMessage } from './types'; - -export const permissionsReadOnlyErrorMessage: ErrorMessage = { - id: 'read-only-privileges-error', - title: i18n.READ_ONLY_FEATURE_TITLE, - description: <>{i18n.READ_ONLY_FEATURE_MSG}, - errorType: 'warning', -}; - export const createCalloutId = (ids: string[], delimiter: string = '|'): string => md5(ids.join(delimiter)); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts index cb7236b445be1..20bb57daf5841 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts @@ -7,21 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_FEATURE_TITLE = i18n.translate( - 'xpack.observability.cases.readOnlyFeatureTitle', - { - defaultMessage: 'You cannot open new or update existing cases', - } -); - -export const READ_ONLY_FEATURE_MSG = i18n.translate( - 'xpack.observability.cases.readOnlyFeatureDescription', - { - defaultMessage: - 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', - } -); - export const DISMISS_CALLOUT = i18n.translate( 'xpack.observability.cases.dismissErrorsPushServiceCallOutTitle', { diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts index 1a5abe218edf5..a85b0bc744e66 100644 --- a/x-pack/plugins/observability/public/components/app/cases/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts @@ -201,3 +201,17 @@ export const CONNECTORS = i18n.translate('xpack.observability.cases.caseView.con export const EDIT_CONNECTOR = i18n.translate('xpack.observability.cases.caseView.editConnector', { defaultMessage: 'Change external incident management system', }); + +export const READ_ONLY_BADGE_TEXT = i18n.translate( + 'xpack.observability.cases.badge.readOnly.text', + { + defaultMessage: 'Read only', + } +); + +export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( + 'xpack.observability.cases.badge.readOnly.tooltip', + { + defaultMessage: 'Unable to create or edit cases', + } +); diff --git a/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx new file mode 100644 index 0000000000000..4d8779e1ea150 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx @@ -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 { useCallback, useEffect } from 'react'; + +import * as i18n from '../components/app/cases/translations'; +import { useGetUserCasesPermissions } from '../hooks/use_get_user_cases_permissions'; +import { useKibana } from '../utils/kibana_react'; + +/** + * This component places a read-only icon badge in the header if user only has read permissions + */ +export function useReadonlyHeader() { + const userPermissions = useGetUserCasesPermissions(); + const chrome = useKibana().services.chrome; + + // if the user is read only then display the glasses badge in the global navigation header + const setBadge = useCallback(() => { + if (userPermissions != null && !userPermissions.crud && userPermissions.read) { + chrome.setBadge({ + text: i18n.READ_ONLY_BADGE_TEXT, + tooltip: i18n.READ_ONLY_BADGE_TOOLTIP, + iconType: 'glasses', + }); + } + }, [chrome, userPermissions]); + + useEffect(() => { + setBadge(); + + // remove the icon after the component unmounts + return () => { + chrome.setBadge(); + }; + }, [setBadge, chrome]); +} diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index f73f3b4cf57d7..442104a710601 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -10,35 +10,28 @@ import React from 'react'; import { AllCases } from '../../components/app/cases/all_cases'; import * as i18n from '../../components/app/cases/translations'; -import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; import { CaseFeatureNoPermissions } from './feature_no_permissions'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useReadonlyHeader } from '../../hooks/use_readonly_header'; import { casesBreadcrumbs } from './links'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; export const AllCasesPage = React.memo(() => { const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); + useReadonlyHeader(); useBreadcrumbs([casesBreadcrumbs.cases]); return userPermissions == null || userPermissions?.read ? ( - <> - {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - - )} - {i18n.PAGE_TITLE}, - }} - > - - - + {i18n.PAGE_TITLE}, + }} + > + + ) : ( ); diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index 6adf5ad286808..f93cb5c4e7919 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -5,45 +5,35 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { CaseView } from '../../components/app/cases/case_view'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { useKibana } from '../../utils/kibana_react'; import { CASES_APP_ID } from '../../components/app/cases/constants'; -import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/app/cases/callout'; +import { useReadonlyHeader } from '../../hooks/use_readonly_header'; export const CaseDetailsPage = React.memo(() => { const { application: { getUrlForApp, navigateToUrl }, } = useKibana().services; + const casesUrl = getUrlForApp(CASES_APP_ID); const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ detailName?: string; subCaseId?: string; }>(); + useReadonlyHeader(); - const casesUrl = getUrlForApp(CASES_APP_ID); - if (userPermissions != null && !userPermissions.read) { - navigateToUrl(casesUrl); - return null; - } + useEffect(() => { + if (userPermissions != null && !userPermissions.read) { + navigateToUrl(casesUrl); + } + }, [casesUrl, navigateToUrl, userPermissions]); return caseId != null ? ( - <> - {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - - )} - - + ) : null; }); diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index a4df4855b0204..9676eb7eba147 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { EuiButtonEmpty } from '@elastic/eui'; @@ -38,10 +38,12 @@ function ConfigureCasesPageComponent() { const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]); - if (userPermissions != null && !userPermissions.read) { - navigateToUrl(casesUrl); - return null; - } + + useEffect(() => { + if (userPermissions != null && !userPermissions.read) { + navigateToUrl(casesUrl); + } + }, [casesUrl, userPermissions, navigateToUrl]); return ( { const { formatUrl } = useFormatUrl(CASES_APP_ID); const href = formatUrl(getCaseUrl()); useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]); - if (userPermissions != null && !userPermissions.crud) { - navigateToUrl(casesUrl); - return null; - } + + useEffect(() => { + if (userPermissions != null && !userPermissions.crud) { + navigateToUrl(casesUrl); + } + }, [casesUrl, navigateToUrl, userPermissions]); return ( { }); it('should not allow user with read only privileges to attach alerts to cases', () => { - cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('be.disabled'); + cy.get(ATTACH_ALERT_TO_CASE_BUTTON).should('not.exist'); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx index 29b17cd426c58..fdd49ad17168d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx @@ -5,18 +5,7 @@ * 2.0. */ -import React from 'react'; import md5 from 'md5'; -import * as i18n from './translations'; -import { ErrorMessage } from './types'; - -export const permissionsReadOnlyErrorMessage: ErrorMessage = { - id: 'read-only-privileges-error', - title: i18n.READ_ONLY_FEATURE_TITLE, - description: <>{i18n.READ_ONLY_FEATURE_MSG}, - errorType: 'warning', -}; - export const createCalloutId = (ids: string[], delimiter: string = '|'): string => md5(ids.join(delimiter)); diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts index db4809126452f..617995cc366b0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts @@ -7,21 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_FEATURE_TITLE = i18n.translate( - 'xpack.securitySolution.cases.readOnlyFeatureTitle', - { - defaultMessage: 'You cannot open new or update existing cases', - } -); - -export const READ_ONLY_FEATURE_MSG = i18n.translate( - 'xpack.securitySolution.cases.readOnlyFeatureDescription', - { - defaultMessage: - 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', - } -); - export const DISMISS_CALLOUT = i18n.translate( 'xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle', { diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 77fa9e8b3cc8c..02047c774ca6f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -200,7 +200,7 @@ describe('AddToCaseAction', () => { ).toBeTruthy(); }); - it('disabled when user does not have crud permissions', () => { + it('hides the icon when user does not have crud permissions', () => { (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ crud: false, read: true, @@ -212,8 +212,6 @@ describe('AddToCaseAction', () => { ); - expect( - wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled') - ).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index eaad912a4dc51..7025bff1ce49a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -208,19 +208,21 @@ const AddToCaseActionComponent: React.FC = ({ return ( <> - - - - - + {userCanCrud && ( + + + + + + )} {isCreateCaseFlyoutOpen && ( { return userPermissions == null || userPermissions?.read ? ( <> - {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - - )} diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 7307733426862..a086409e55df5 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { SecurityPageName } from '../../app/types'; @@ -16,7 +16,6 @@ import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; import { getCaseUrl } from '../../common/components/link_to'; import { navTabs } from '../../app/home/home_navigations'; import { CaseView } from '../components/case_view'; -import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; import { CASES_APP_ID } from '../../../common/constants'; export const CaseDetailsPage = React.memo(() => { @@ -30,20 +29,15 @@ export const CaseDetailsPage = React.memo(() => { }>(); const search = useGetUrlSearch(navTabs.case); - if (userPermissions != null && !userPermissions.read) { - navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) }); - return null; - } + useEffect(() => { + if (userPermissions != null && !userPermissions.read) { + navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) }); + } + }, [navigateToApp, userPermissions, search]); return caseId != null ? ( <> - {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - - )} { [search] ); - if (userPermissions != null && !userPermissions.read) { - navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) }); - return null; - } + useEffect(() => { + if (userPermissions != null && !userPermissions.read) { + navigateToApp(CASES_APP_ID, { + path: getCaseUrl(search), + }); + } + }, [navigateToApp, userPermissions, search]); const HeaderWrapper = styled.div` padding-top: ${({ theme }) => theme.eui.paddingSizes.l}; diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx index 19f97bae60ebe..3c5197f19eff1 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; @@ -25,6 +25,7 @@ export const CreateCasePage = React.memo(() => { const { application: { navigateToApp }, } = useKibana().services; + const backOptions = useMemo( () => ({ href: getCaseUrl(search), @@ -34,12 +35,13 @@ export const CreateCasePage = React.memo(() => { [search] ); - if (userPermissions != null && !userPermissions.crud) { - navigateToApp(CASES_APP_ID, { - path: getCaseUrl(search), - }); - return null; - } + useEffect(() => { + if (userPermissions != null && !userPermissions.crud) { + navigateToApp(CASES_APP_ID, { + path: getCaseUrl(search), + }); + } + }, [userPermissions, navigateToApp, search]); return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx new file mode 100644 index 0000000000000..0d12d63fdc244 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { BrowserRouter as Router } from 'react-router-dom'; + +import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; +import { TestProviders } from '../../common/mock'; +import { Case } from '.'; + +const useKibanaMock = useKibana as jest.Mocked; +jest.mock('../../common/lib/kibana'); + +const mockedSetBadge = jest.fn(); + +describe('CaseContainerComponent', () => { + beforeEach(() => { + jest.clearAllMocks(); + useKibanaMock().services.chrome.setBadge = mockedSetBadge; + }); + + it('does not display the readonly glasses badge when the user has write permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: true, + read: false, + }); + + mount( + + + + + + ); + + expect(mockedSetBadge).not.toBeCalled(); + }); + + it('does not display the readonly glasses badge when the user has neither write nor read permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: false, + }); + + mount( + + + + + + ); + + expect(mockedSetBadge).not.toBeCalled(); + }); + + it('does not display the readonly glasses badge when the user has null permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue(null); + + mount( + + + + + + ); + + expect(mockedSetBadge).not.toBeCalled(); + }); + + it('displays the readonly glasses badge read permissions but not write', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: true, + }); + + mount( + + + + + + ); + + expect(mockedSetBadge).toBeCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 314bdc9bfd117..fca19cf5c70a7 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -5,13 +5,15 @@ * 2.0. */ -import React from 'react'; - +import React, { useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; + +import * as i18n from './translations'; import { CaseDetailsPage } from './case_details'; import { CasesPage } from './case'; import { CreateCasePage } from './create_case'; import { ConfigureCasesPage } from './configure_cases'; +import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; const casesPagePath = ''; const caseDetailsPagePath = `${casesPagePath}/:detailName`; @@ -21,30 +23,51 @@ const subCaseDetailsPagePathWithCommentId = `${subCaseDetailsPagePath}/:commentI const createCasePagePath = `${casesPagePath}/create`; const configureCasesPagePath = `${casesPagePath}/configure`; -const CaseContainerComponent: React.FC = () => ( - - - - - - - - - - - - - - - - - - - - - - - -); +const CaseContainerComponent: React.FC = () => { + const userPermissions = useGetUserCasesPermissions(); + const chrome = useKibana().services.chrome; + + useEffect(() => { + // if the user is read only then display the glasses badge in the global navigation header + if (userPermissions != null && !userPermissions.crud && userPermissions.read) { + chrome.setBadge({ + text: i18n.READ_ONLY_BADGE_TEXT, + tooltip: i18n.READ_ONLY_BADGE_TOOLTIP, + iconType: 'glasses', + }); + } + + // remove the icon after the component unmounts + return () => { + chrome.setBadge(); + }; + }, [userPermissions, chrome]); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; export const Case = React.memo(CaseContainerComponent); diff --git a/x-pack/plugins/security_solution/public/cases/pages/translations.ts b/x-pack/plugins/security_solution/public/cases/pages/translations.ts index 1a811a3fd7bbc..6768401b3f608 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/pages/translations.ts @@ -157,3 +157,24 @@ export const GO_TO_DOCUMENTATION = i18n.translate( export const CONNECTORS = i18n.translate('xpack.securitySolution.cases.caseView.connectors', { defaultMessage: 'External Incident Management System', }); + +export const EDIT_CONNECTOR = i18n.translate( + 'xpack.securitySolution.cases.caseView.editConnector', + { + defaultMessage: 'Change external incident management system', + } +); + +export const READ_ONLY_BADGE_TEXT = i18n.translate( + 'xpack.securitySolution.cases.badge.readOnly.text', + { + defaultMessage: 'Read only', + } +); + +export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.cases.badge.readOnly.tooltip', + { + defaultMessage: 'Unable to create or edit cases', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx new file mode 100644 index 0000000000000..96a7eacb7fb08 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { useGetUserCasesPermissions } from '../../../common/lib/kibana'; +import { TestProviders } from '../../../common/mock'; +import { HeaderGlobal } from '.'; + +jest.mock('../../../common/lib/kibana'); + +describe('HeaderGlobal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not display the cases tab when the user does not have read permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: false, + }); + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeFalsy(); + }); + + it('displays the cases tab when the user has read permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: true, + read: true, + }); + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index 4a7ac8a148f64..e91905183aab1 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -19,7 +19,7 @@ import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { useKibana } from '../../lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { LinkAnchor } from '../links'; @@ -91,6 +91,18 @@ export const HeaderGlobal = React.memo( }, [navigateToApp, search] ); + + const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; + + // build a list of tabs to exclude + const tabsToExclude = new Set([ + ...(hideDetectionEngine ? [SecurityPageName.detections] : []), + ...(!hasCasesReadPermissions ? [SecurityPageName.case] : []), + ]); + + // include the tab if it is not in the set of excluded ones + const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs); + return ( @@ -109,14 +121,7 @@ export const HeaderGlobal = React.memo( - key !== SecurityPageName.detections, navTabs) - : navTabs - } - /> + diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx index 996835296fcc4..cb7733e304985 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx @@ -13,7 +13,7 @@ import { getCreateCaseUrl, } from '../../../common/components/link_to/redirect_to_case'; import { useFormatUrl } from '../../../common/components/link_to'; -import { useKibana } from '../../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; import { APP_ID, CASES_APP_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; import { AllCasesNavProps } from '../../../cases/components/all_cases'; @@ -26,6 +26,8 @@ const RecentCasesComponent = () => { application: { navigateToApp }, } = useKibana().services; + const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false; + return casesUi.getRecentCases({ allCasesNavigation: { href: formatUrl(getCaseUrl()), @@ -60,6 +62,7 @@ const RecentCasesComponent = () => { }); }, }, + hasWritePermissions, maxCasesToShow: MAX_CASES_TO_SHOW, owner: [APP_ID], }); diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx new file mode 100644 index 0000000000000..76c5663644a78 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx @@ -0,0 +1,72 @@ +/* + * 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 } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { Sidebar } from './sidebar'; +import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; +import { casesPluginMock } from '../../../../../cases/public/mocks'; +import { CasesUiStart } from '../../../../../cases/public'; + +jest.mock('../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.MockedFunction; + +describe('Sidebar', () => { + let casesMock: jest.Mocked; + + beforeEach(() => { + casesMock = casesPluginMock.createStartContract(); + casesMock.getRecentCases.mockImplementation(() => <>{'test'}); + useKibanaMock.mockReturnValue(({ + services: { + cases: casesMock, + application: { + // these are needed by the RecentCases component if it is rendered. + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(() => ''), + }, + }, + } as unknown) as ReturnType); + }); + + it('does not render the recently created cases section when the user does not have read permissions', async () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: false, + }); + + await waitFor(() => + mount( + + {}} /> + + ) + ); + + expect(casesMock.getRecentCases).not.toHaveBeenCalled(); + }); + + it('does render the recently created cases section when the user has read permissions', async () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: true, + }); + + await waitFor(() => + mount( + + {}} /> + + ) + ); + + expect(casesMock.getRecentCases).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx index 77cfa220f0722..b8701f3ef1639 100644 --- a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx @@ -18,6 +18,7 @@ import { SidebarHeader } from '../../../common/components/sidebar_header'; import * as i18n from '../../pages/translations'; import { RecentCases } from '../recent_cases'; +import { useGetUserCasesPermissions } from '../../../common/lib/kibana'; const SidebarFlexGroup = styled(EuiFlexGroup)` width: 305px; @@ -46,13 +47,20 @@ export const Sidebar = React.memo<{ [recentTimelinesFilterBy, setRecentTimelinesFilterBy] ); + // only render the recently created cases view if the user has at least read permissions + const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; + return ( - - - + {hasCasesReadPermissions && ( + <> + + + - + + + )} {recentTimelinesFilters} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx index 68b4f2e4a0c31..206fcb2dc087c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { useKibana } from '../../../../common/lib/kibana'; +import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; import { TimelineId } from '../../../../../common/types/timeline'; import { useTimelineKpis } from '../../../containers/kpis'; @@ -57,7 +57,7 @@ const defaultMocks = { loading: false, selectedPatterns: mockIndexNames, }; -describe('Timeline KPIs', () => { +describe('header', () => { const mount = useMountAppended(); beforeEach(() => { @@ -75,86 +75,124 @@ describe('Timeline KPIs', () => { jest.clearAllMocks(); }); - describe('when the data is not loading and the response contains data', () => { + describe('AddToCaseButton', () => { beforeEach(() => { mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); }); - it('renders the component, labels and values succesfully', async () => { + + it('renders the button when the user has write permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: true, + read: false, + }); + const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true); - // label - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('Processes') - ); - // value - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('1') - ); - }); - }); - describe('when the data is loading', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]); + expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeTruthy(); }); - it('renders a loading indicator for values', async () => { + + it('does not render the button when the user does not have write permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: false, + }); + const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('--') - ); + + expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeFalsy(); }); }); - describe('when the response is null and timeline is blank', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, null]); + describe('Timeline KPIs', () => { + describe('when the data is not loading and the response contains data', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); + }); + it('renders the component, labels and values successfully', async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true); + // label + expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( + expect.stringContaining('Processes') + ); + // value + expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( + expect.stringContaining('1') + ); + }); }); - it('renders labels and the default empty string', async () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('Processes') - ); - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining(getEmptyValue()) - ); + describe('when the data is loading', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]); + }); + it('renders a loading indicator for values', async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( + expect.stringContaining('--') + ); + }); }); - }); - describe('when the response contains numbers larger than one thousand', () => { - beforeEach(() => { - mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); + describe('when the response is null and timeline is blank', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, null]); + }); + it('renders labels and the default empty string', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( + expect.stringContaining('Processes') + ); + expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( + expect.stringContaining(getEmptyValue()) + ); + }); }); - it('formats the numbers correctly', async () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('1k') - ); - expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual( - expect.stringContaining('1m') - ); - expect(wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text()).toEqual( - expect.stringContaining('1b') - ); - expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual( - expect.stringContaining('999') - ); + + describe('when the response contains numbers larger than one thousand', () => { + beforeEach(() => { + mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); + }); + it('formats the numbers correctly', async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( + expect.stringContaining('1k') + ); + expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual( + expect.stringContaining('1m') + ); + expect( + wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text() + ).toEqual(expect.stringContaining('1b')); + expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual( + expect.stringContaining('999') + ); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index dd8cdb818cad7..216282b72920c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -35,7 +35,7 @@ import { TimerangeInput } from '../../../../../common/search_strategy'; import { AddToCaseButton } from '../add_to_case_button'; import { AddTimelineButton } from '../add_timeline_button'; import { SaveTimelineButton } from '../../timeline/header/save_timeline_button'; -import { useKibana } from '../../../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; import { InspectButton } from '../../../../common/components/inspect'; import { useTimelineKpis } from '../../../containers/kpis'; import { esQuery } from '../../../../../../../../src/plugins/data/public'; @@ -319,6 +319,8 @@ const FlyoutHeaderComponent: React.FC = ({ timelineId }) => { filterQuery: combinedQueries?.filterQuery ?? '', }); + const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false; + return ( @@ -350,9 +352,11 @@ const FlyoutHeaderComponent: React.FC = ({ timelineId }) => { - - - + {hasWritePermissions && ( + + + + )} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a0f466512cc1d..b3d37958a324e 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -129,17 +129,23 @@ export interface PluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} -const securitySubPlugins = [ +const casesSubPlugin = `${APP_ID}:${SecurityPageName.case}`; + +/** + * Don't include cases here so that the sub feature can govern whether Cases is enabled in the navigation + */ +const securitySubPluginsNoCases = [ APP_ID, `${APP_ID}:${SecurityPageName.overview}`, `${APP_ID}:${SecurityPageName.detections}`, `${APP_ID}:${SecurityPageName.hosts}`, `${APP_ID}:${SecurityPageName.network}`, `${APP_ID}:${SecurityPageName.timelines}`, - `${APP_ID}:${SecurityPageName.case}`, `${APP_ID}:${SecurityPageName.administration}`, ]; +const allSecuritySubPlugins = [...securitySubPluginsNoCases, casesSubPlugin]; + export class Plugin implements IPlugin { private readonly logger: Logger; private readonly config: ConfigType; @@ -303,7 +309,7 @@ export class Plugin implements IPlugin { await PageObjects.common.navigateToActualUrl('observabilityCases'); - await PageObjects.observability.expectCreateCaseButtonDisabled(); + await PageObjects.observability.expectCreateCaseButtonMissing(); }); - it(`shows read-only callout`, async () => { - await PageObjects.observability.expectReadOnlyCallout(); + it(`shows read-only glasses badge`, async () => { + await PageObjects.observability.expectReadOnlyGlassesBadge(); }); it(`does not allow a case to be created`, async () => { @@ -151,7 +151,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); // expect redirection to observability cases landing - await PageObjects.observability.expectCreateCaseButtonDisabled(); + await PageObjects.observability.expectCreateCaseButtonMissing(); }); it(`does not allow a case to be edited`, async () => { @@ -162,7 +162,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, } ); - await PageObjects.observability.expectAddCommentButtonDisabled(); + await PageObjects.observability.expectAddCommentButtonMissing(); }); }); diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts index 95016c31d1054..d9e413d473adf 100644 --- a/x-pack/test/functional/page_objects/observability_page.ts +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -20,14 +20,12 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro expect(disabledAttr).to.be(null); }, - async expectCreateCaseButtonDisabled() { - const button = await testSubjects.find('createNewCaseBtn', 20000); - const disabledAttr = await button.getAttribute('disabled'); - expect(disabledAttr).to.be('true'); + async expectCreateCaseButtonMissing() { + await testSubjects.missingOrFail('createNewCaseBtn'); }, - async expectReadOnlyCallout() { - await testSubjects.existOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3'); + async expectReadOnlyGlassesBadge() { + await testSubjects.existOrFail('headerBadge'); }, async expectNoReadOnlyCallout() { @@ -44,10 +42,8 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro expect(disabledAttr).to.be(null); }, - async expectAddCommentButtonDisabled() { - const button = await testSubjects.find('submit-comment', 20000); - const disabledAttr = await button.getAttribute('disabled'); - expect(disabledAttr).to.be('true'); + async expectAddCommentButtonMissing() { + await testSubjects.missingOrFail('submit-comment'); }, async expectForbidden() { From 3d4521b32e08051153eab9d2b863c535a102daa5 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:10:33 -0400 Subject: [PATCH 073/118] [Security Solution] show case names in isolation success message (#102664) (#102974) # Conflicts: # x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts --- x-pack/plugins/cases/common/api/cases/case.ts | 25 ++++++++ .../plugins/cases/common/api/cases/comment.ts | 16 ----- .../classes/client.casesclient.md | 28 ++++----- .../interfaces/attachments_add.addargs.md | 4 +- ...attachments_client.attachmentssubclient.md | 16 ++--- .../attachments_delete.deleteallargs.md | 4 +- .../attachments_delete.deleteargs.md | 6 +- .../interfaces/attachments_get.findargs.md | 4 +- ...ttachments_get.getallalertsattachtocase.md | 2 +- .../interfaces/attachments_get.getallargs.md | 6 +- .../interfaces/attachments_get.getargs.md | 4 +- .../attachments_update.updateargs.md | 6 +- .../interfaces/cases_client.casessubclient.md | 30 +++++----- .../cases_get.caseidsbyalertidparams.md | 40 ------------- .../cases_get.casesbyalertidparams.md | 40 +++++++++++++ .../interfaces/cases_get.getparams.md | 6 +- .../interfaces/cases_push.pushparams.md | 4 +- .../configure_client.configuresubclient.md | 8 +-- .../interfaces/stats_client.statssubclient.md | 2 +- .../sub_cases_client.subcasesclient.md | 8 +-- .../user_actions_client.useractionget.md | 4 +- ...ser_actions_client.useractionssubclient.md | 2 +- .../docs/cases_client/modules/cases_get.md | 6 +- .../cases/server/client/cases/client.ts | 12 ++-- .../plugins/cases/server/client/cases/get.ts | 47 +++++++++++++-- x-pack/plugins/cases/server/client/mocks.ts | 2 +- .../routes/api/cases/alerts/get_cases.ts | 4 +- .../plugins/cases/server/routes/api/index.ts | 4 +- .../components/host_isolation/index.tsx | 20 +++---- .../components/host_isolation/isolate.tsx | 11 +++- .../components/host_isolation/unisolate.tsx | 11 +++- .../detection_engine/alerts/mock.ts | 4 +- .../detection_engine/alerts/types.ts | 2 +- .../alerts/use_cases_from_alerts.test.tsx | 2 +- .../alerts/use_cases_from_alerts.tsx | 4 +- .../endpoint/routes/actions/isolation.ts | 16 +++-- .../case_api_integration/common/lib/utils.ts | 5 +- .../common/lib/validation.ts | 27 +++++++++ .../tests/common/alerts/get_cases.ts | 58 +++++++++---------- .../tests/common/alerts/get_cases.ts | 38 ++++++------ .../tests/common/alerts/get_cases.ts | 17 +++--- 41 files changed, 319 insertions(+), 236 deletions(-) delete mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md create mode 100644 x-pack/test/case_api_integration/common/lib/validation.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index b3f7952a61ee7..a72eda5bb1207 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -14,6 +14,28 @@ import { CasesStatusResponseRt, CaseStatusRt } from './status'; import { CaseConnectorRt, ESCaseConnector } from '../connectors'; import { SubCaseResponseRt } from './sub_case'; +const BucketsAggs = rt.array( + rt.type({ + key: rt.string, + }) +); + +export const GetCaseIdsByAlertIdAggsRt = rt.type({ + references: rt.type({ + doc_count: rt.number, + caseIds: rt.type({ + buckets: BucketsAggs, + }), + }), +}); + +export const CasesByAlertIdRt = rt.array( + rt.type({ + id: rt.string, + title: rt.string, + }) +); + export enum CaseType { collection = 'collection', individual = 'individual', @@ -311,3 +333,6 @@ export type ESCasePatchRequest = Omit & { export type AllTagsFindRequest = rt.TypeOf; export type AllReportersFindRequest = AllTagsFindRequest; + +export type GetCaseIdsByAlertIdAggs = rt.TypeOf; +export type CasesByAlertId = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 5bc8da95639c8..746c28f994239 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -10,21 +10,6 @@ import { SavedObjectFindOptionsRt } from '../saved_object'; import { UserRT } from '../user'; -const BucketsAggs = rt.array( - rt.type({ - key: rt.string, - }) -); - -export const GetCaseIdsByAlertIdAggsRt = rt.type({ - references: rt.type({ - doc_count: rt.number, - caseIds: rt.type({ - buckets: BucketsAggs, - }), - }), -}); - /** * this is used to differentiate between an alert attached to a top-level case and a group of alerts that should only * be attached to a sub case. The reason we need this is because an alert group comment will have references to both a case and @@ -152,4 +137,3 @@ export type CommentPatchRequest = rt.TypeOf; export type CommentPatchAttributes = rt.TypeOf; export type CommentRequestUserType = rt.TypeOf; export type CommentRequestAlertType = rt.TypeOf; -export type GetCaseIdsByAlertIdAggs = rt.TypeOf; diff --git a/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md index a20f018cffeb8..bd07a44a2bfdf 100644 --- a/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md +++ b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md @@ -45,7 +45,7 @@ Client wrapper that contains accessor methods for individual entities within the **Returns:** [*CasesClient*](client.casesclient.md) -Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L28) +Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L28) ## Properties @@ -53,7 +53,7 @@ Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/0e98e • `Private` `Readonly` **\_attachments**: [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md) -Defined in: [client.ts:24](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L24) +Defined in: [client.ts:24](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L24) ___ @@ -61,7 +61,7 @@ ___ • `Private` `Readonly` **\_cases**: [*CasesSubClient*](../interfaces/cases_client.casessubclient.md) -Defined in: [client.ts:23](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L23) +Defined in: [client.ts:23](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L23) ___ @@ -69,7 +69,7 @@ ___ • `Private` `Readonly` **\_casesClientInternal**: *CasesClientInternal* -Defined in: [client.ts:22](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L22) +Defined in: [client.ts:22](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L22) ___ @@ -77,7 +77,7 @@ ___ • `Private` `Readonly` **\_configure**: [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md) -Defined in: [client.ts:27](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L27) +Defined in: [client.ts:27](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L27) ___ @@ -85,7 +85,7 @@ ___ • `Private` `Readonly` **\_stats**: [*StatsSubClient*](../interfaces/stats_client.statssubclient.md) -Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L28) +Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L28) ___ @@ -93,7 +93,7 @@ ___ • `Private` `Readonly` **\_subCases**: [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md) -Defined in: [client.ts:26](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L26) +Defined in: [client.ts:26](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L26) ___ @@ -101,7 +101,7 @@ ___ • `Private` `Readonly` **\_userActions**: [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md) -Defined in: [client.ts:25](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L25) +Defined in: [client.ts:25](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L25) ## Accessors @@ -113,7 +113,7 @@ Retrieves an interface for interacting with attachments (comments) entities. **Returns:** [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md) -Defined in: [client.ts:50](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L50) +Defined in: [client.ts:50](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L50) ___ @@ -125,7 +125,7 @@ Retrieves an interface for interacting with cases entities. **Returns:** [*CasesSubClient*](../interfaces/cases_client.casessubclient.md) -Defined in: [client.ts:43](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L43) +Defined in: [client.ts:43](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L43) ___ @@ -137,7 +137,7 @@ Retrieves an interface for interacting with the configuration of external connec **Returns:** [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md) -Defined in: [client.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L76) +Defined in: [client.ts:76](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L76) ___ @@ -149,7 +149,7 @@ Retrieves an interface for retrieving statistics related to the cases entities. **Returns:** [*StatsSubClient*](../interfaces/stats_client.statssubclient.md) -Defined in: [client.ts:83](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L83) +Defined in: [client.ts:83](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L83) ___ @@ -163,7 +163,7 @@ Currently this functionality is disabled and will throw an error if this functio **Returns:** [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md) -Defined in: [client.ts:66](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L66) +Defined in: [client.ts:66](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L66) ___ @@ -175,4 +175,4 @@ Retrieves an interface for interacting with the user actions associated with the **Returns:** [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md) -Defined in: [client.ts:57](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L57) +Defined in: [client.ts:57](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L57) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md index d5233ab6d8cb4..f8f7babd15b90 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md @@ -21,7 +21,7 @@ The arguments needed for creating a new attachment to a case. The case ID that this attachment will be associated with -Defined in: [attachments/add.ts:305](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/add.ts#L305) +Defined in: [attachments/add.ts:305](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/add.ts#L305) ___ @@ -31,4 +31,4 @@ ___ The attachment values. -Defined in: [attachments/add.ts:309](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/add.ts#L309) +Defined in: [attachments/add.ts:309](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/add.ts#L309) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md index 1a9a687aa812b..57141796f6f67 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md @@ -35,7 +35,7 @@ Adds an attachment to a case. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [attachments/client.ts:35](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L35) +Defined in: [attachments/client.ts:35](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L35) ___ @@ -53,7 +53,7 @@ Deletes a single attachment for a specific case. **Returns:** *Promise* -Defined in: [attachments/client.ts:43](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L43) +Defined in: [attachments/client.ts:43](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L43) ___ @@ -71,7 +71,7 @@ Deletes all attachments associated with a single case. **Returns:** *Promise* -Defined in: [attachments/client.ts:39](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L39) +Defined in: [attachments/client.ts:39](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L39) ___ @@ -89,7 +89,7 @@ Retrieves all comments matching the search criteria. **Returns:** *Promise*<[*ICommentsResponse*](typedoc_interfaces.icommentsresponse.md)\> -Defined in: [attachments/client.ts:47](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L47) +Defined in: [attachments/client.ts:47](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L47) ___ @@ -107,7 +107,7 @@ Retrieves a single attachment for a case. **Returns:** *Promise*<{ `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }\> -Defined in: [attachments/client.ts:59](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L59) +Defined in: [attachments/client.ts:59](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L59) ___ @@ -125,7 +125,7 @@ Gets all attachments for a single case. **Returns:** *Promise*<[*IAllCommentsResponse*](typedoc_interfaces.iallcommentsresponse.md)\> -Defined in: [attachments/client.ts:55](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L55) +Defined in: [attachments/client.ts:55](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L55) ___ @@ -143,7 +143,7 @@ Retrieves all alerts attach to a case given a single case ID **Returns:** *Promise*<{ `attached_at`: *string* ; `id`: *string* ; `index`: *string* }[]\> -Defined in: [attachments/client.ts:51](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L51) +Defined in: [attachments/client.ts:51](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L51) ___ @@ -163,4 +163,4 @@ The request must include all fields for the attachment. Even the fields that are **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [attachments/client.ts:65](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L65) +Defined in: [attachments/client.ts:65](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L65) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md index 437758a0147f2..d134c92e282a3 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md @@ -21,7 +21,7 @@ Parameters for deleting all comments of a case or sub case. The case ID to delete all attachments for -Defined in: [attachments/delete.ts:31](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L31) +Defined in: [attachments/delete.ts:31](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L31) ___ @@ -31,4 +31,4 @@ ___ If specified the caseID will be ignored and this value will be used to find a sub case for deleting all the attachments -Defined in: [attachments/delete.ts:35](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L35) +Defined in: [attachments/delete.ts:35](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L35) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md index 1afa5679161d9..a1c177bad8a09 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md @@ -22,7 +22,7 @@ Parameters for deleting a single attachment of a case or sub case. The attachment ID to delete -Defined in: [attachments/delete.ts:49](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L49) +Defined in: [attachments/delete.ts:49](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L49) ___ @@ -32,7 +32,7 @@ ___ The case ID to delete an attachment from -Defined in: [attachments/delete.ts:45](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L45) +Defined in: [attachments/delete.ts:45](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L45) ___ @@ -42,4 +42,4 @@ ___ If specified the caseID will be ignored and this value will be used to find a sub case for deleting the attachment -Defined in: [attachments/delete.ts:53](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L53) +Defined in: [attachments/delete.ts:53](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L53) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md index dc0da295b26d2..dcd4deb28b687 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md @@ -21,7 +21,7 @@ Parameters for finding attachments of a case The case ID for finding associated attachments -Defined in: [attachments/get.ts:47](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L47) +Defined in: [attachments/get.ts:47](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L47) ___ @@ -48,4 +48,4 @@ Optional parameters for filtering the returned attachments | `sortOrder` | *undefined* \| ``"desc"`` \| ``"asc"`` | | `subCaseId` | *undefined* \| *string* | -Defined in: [attachments/get.ts:51](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L51) +Defined in: [attachments/get.ts:51](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L51) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md index 541d1cf8f1d80..d935823054b03 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md @@ -18,4 +18,4 @@ The ID of the case to retrieve the alerts from -Defined in: [attachments/get.ts:87](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L87) +Defined in: [attachments/get.ts:87](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L87) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md index ae67f85e96fc0..9577e89b46074 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md @@ -22,7 +22,7 @@ Parameters for retrieving all attachments of a case The case ID to retrieve all attachments for -Defined in: [attachments/get.ts:61](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L61) +Defined in: [attachments/get.ts:61](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L61) ___ @@ -32,7 +32,7 @@ ___ Optionally include the attachments associated with a sub case -Defined in: [attachments/get.ts:65](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L65) +Defined in: [attachments/get.ts:65](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L65) ___ @@ -42,4 +42,4 @@ ___ If included the case ID will be ignored and the attachments will be retrieved from the specified ID of the sub case -Defined in: [attachments/get.ts:69](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L69) +Defined in: [attachments/get.ts:69](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L69) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md index 2fc569985f980..5530ad8bd936e 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md @@ -19,7 +19,7 @@ The ID of the attachment to retrieve -Defined in: [attachments/get.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L80) +Defined in: [attachments/get.ts:80](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L80) ___ @@ -29,4 +29,4 @@ ___ The ID of the case to retrieve an attachment from -Defined in: [attachments/get.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L76) +Defined in: [attachments/get.ts:76](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L76) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md index 4b2dd7b404e7a..ce586a6bfdfbd 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md @@ -22,7 +22,7 @@ Parameters for updating a single attachment The ID of the case that is associated with this attachment -Defined in: [attachments/update.ts:32](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/update.ts#L32) +Defined in: [attachments/update.ts:32](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/update.ts#L32) ___ @@ -32,7 +32,7 @@ ___ The ID of a sub case, if specified a sub case will be searched for to perform the attachment update instead of on a case -Defined in: [attachments/update.ts:40](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/update.ts#L40) +Defined in: [attachments/update.ts:40](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/update.ts#L40) ___ @@ -42,4 +42,4 @@ ___ The full attachment request with the fields updated with appropriate values -Defined in: [attachments/update.ts:36](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/update.ts#L36) +Defined in: [attachments/update.ts:36](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/update.ts#L36) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md index d86308720cb95..52cf2fbaf1ef1 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md @@ -14,7 +14,7 @@ API for interacting with the cases entities. - [delete](cases_client.casessubclient.md#delete) - [find](cases_client.casessubclient.md#find) - [get](cases_client.casessubclient.md#get) -- [getCaseIDsByAlertID](cases_client.casessubclient.md#getcaseidsbyalertid) +- [getCasesByAlertID](cases_client.casessubclient.md#getcasesbyalertid) - [getReporters](cases_client.casessubclient.md#getreporters) - [getTags](cases_client.casessubclient.md#gettags) - [push](cases_client.casessubclient.md#push) @@ -36,7 +36,7 @@ Creates a case. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [cases/client.ts:48](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L48) +Defined in: [cases/client.ts:49](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L49) ___ @@ -56,7 +56,7 @@ Delete a case and all its comments. **Returns:** *Promise* -Defined in: [cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L72) +Defined in: [cases/client.ts:73](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L73) ___ @@ -76,7 +76,7 @@ If the `owner` field is left empty then all the cases that the user has access t **Returns:** *Promise*<[*ICasesFindResponse*](typedoc_interfaces.icasesfindresponse.md)\> -Defined in: [cases/client.ts:54](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L54) +Defined in: [cases/client.ts:55](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L55) ___ @@ -94,25 +94,25 @@ Retrieves a single case with the specified ID. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [cases/client.ts:58](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L58) +Defined in: [cases/client.ts:59](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L59) ___ -### getCaseIDsByAlertID +### getCasesByAlertID -▸ **getCaseIDsByAlertID**(`params`: [*CaseIDsByAlertIDParams*](cases_get.caseidsbyalertidparams.md)): *Promise* +▸ **getCasesByAlertID**(`params`: [*CasesByAlertIDParams*](cases_get.casesbyalertidparams.md)): *Promise*<{ `id`: *string* ; `title`: *string* }[]\> -Retrieves the case IDs given a single alert ID +Retrieves the cases ID and title that have the requested alert attached to them #### Parameters | Name | Type | | :------ | :------ | -| `params` | [*CaseIDsByAlertIDParams*](cases_get.caseidsbyalertidparams.md) | +| `params` | [*CasesByAlertIDParams*](cases_get.casesbyalertidparams.md) | -**Returns:** *Promise* +**Returns:** *Promise*<{ `id`: *string* ; `title`: *string* }[]\> -Defined in: [cases/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L84) +Defined in: [cases/client.ts:85](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L85) ___ @@ -131,7 +131,7 @@ Retrieves all the reporters across all accessible cases. **Returns:** *Promise*<{ `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* }[]\> -Defined in: [cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L80) +Defined in: [cases/client.ts:81](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L81) ___ @@ -150,7 +150,7 @@ Retrieves all the tags across all cases the user making the request has access t **Returns:** *Promise* -Defined in: [cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L76) +Defined in: [cases/client.ts:77](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L77) ___ @@ -168,7 +168,7 @@ Pushes a specific case to an external system. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [cases/client.ts:62](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L62) +Defined in: [cases/client.ts:63](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L63) ___ @@ -186,4 +186,4 @@ Update the specified cases with the passed in values. **Returns:** *Promise*<[*ICasesResponse*](typedoc_interfaces.icasesresponse.md)\> -Defined in: [cases/client.ts:66](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L66) +Defined in: [cases/client.ts:67](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L67) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md deleted file mode 100644 index 274b7a8f2d431..0000000000000 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md +++ /dev/null @@ -1,40 +0,0 @@ -[Cases Client API Interface](../cases_client_api.md) / [cases/get](../modules/cases_get.md) / CaseIDsByAlertIDParams - -# Interface: CaseIDsByAlertIDParams - -[cases/get](../modules/cases_get.md).CaseIDsByAlertIDParams - -Parameters for finding cases IDs using an alert ID - -## Table of contents - -### Properties - -- [alertID](cases_get.caseidsbyalertidparams.md#alertid) -- [options](cases_get.caseidsbyalertidparams.md#options) - -## Properties - -### alertID - -• **alertID**: *string* - -The alert ID to search for - -Defined in: [cases/get.ts:42](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L42) - -___ - -### options - -• **options**: *object* - -The filtering options when searching for associated cases. - -#### Type declaration - -| Name | Type | -| :------ | :------ | -| `owner` | *undefined* \| *string* \| *string*[] | - -Defined in: [cases/get.ts:46](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L46) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md new file mode 100644 index 0000000000000..4992ed035721b --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md @@ -0,0 +1,40 @@ +[Cases Client API Interface](../cases_client_api.md) / [cases/get](../modules/cases_get.md) / CasesByAlertIDParams + +# Interface: CasesByAlertIDParams + +[cases/get](../modules/cases_get.md).CasesByAlertIDParams + +Parameters for finding cases IDs using an alert ID + +## Table of contents + +### Properties + +- [alertID](cases_get.casesbyalertidparams.md#alertid) +- [options](cases_get.casesbyalertidparams.md#options) + +## Properties + +### alertID + +• **alertID**: *string* + +The alert ID to search for + +Defined in: [cases/get.ts:44](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L44) + +___ + +### options + +• **options**: *object* + +The filtering options when searching for associated cases. + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `owner` | *undefined* \| *string* \| *string*[] | + +Defined in: [cases/get.ts:48](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L48) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md index a528b7ce6256d..a4dfc7301e543 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md @@ -22,7 +22,7 @@ The parameters for retrieving a case Case ID -Defined in: [cases/get.ts:110](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L110) +Defined in: [cases/get.ts:145](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L145) ___ @@ -32,7 +32,7 @@ ___ Whether to include the attachments for a case in the response -Defined in: [cases/get.ts:114](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L114) +Defined in: [cases/get.ts:149](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L149) ___ @@ -42,4 +42,4 @@ ___ Whether to include the attachments for all children of a case in the response -Defined in: [cases/get.ts:118](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L118) +Defined in: [cases/get.ts:153](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L153) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md index 979e30cb31d3f..0ed510700af8a 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md @@ -21,7 +21,7 @@ Parameters for pushing a case to an external system The ID of a case -Defined in: [cases/push.ts:53](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/push.ts#L53) +Defined in: [cases/push.ts:53](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/push.ts#L53) ___ @@ -31,4 +31,4 @@ ___ The ID of an external system to push to -Defined in: [cases/push.ts:57](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/push.ts#L57) +Defined in: [cases/push.ts:57](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/push.ts#L57) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md index cf69b101ce2bc..98a6c3a2fcbbf 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md @@ -31,7 +31,7 @@ Creates a configuration if one does not already exist. If one exists it is delet **Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> -Defined in: [configure/client.ts:98](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L98) +Defined in: [configure/client.ts:98](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L98) ___ @@ -50,7 +50,7 @@ Retrieves the external connector configuration for a particular case owner. **Returns:** *Promise*<{} \| [*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> -Defined in: [configure/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L80) +Defined in: [configure/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L80) ___ @@ -62,7 +62,7 @@ Retrieves the valid external connectors supported by the cases plugin. **Returns:** *Promise* -Defined in: [configure/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L84) +Defined in: [configure/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L84) ___ @@ -81,4 +81,4 @@ Updates a particular configuration with new values. **Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> -Defined in: [configure/client.ts:91](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L91) +Defined in: [configure/client.ts:91](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L91) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md index 761b34b5205ec..cc0f30055597d 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md @@ -29,4 +29,4 @@ Retrieves the total number of open, closed, and in-progress cases. **Returns:** *Promise*<{ `count_closed_cases`: *number* ; `count_in_progress_cases`: *number* ; `count_open_cases`: *number* }\> -Defined in: [stats/client.ts:34](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/stats/client.ts#L34) +Defined in: [stats/client.ts:34](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/stats/client.ts#L34) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md index c83c68620e8ac..5c0369709c0f0 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md @@ -31,7 +31,7 @@ Deletes the specified entities and their attachments. **Returns:** *Promise* -Defined in: [sub_cases/client.ts:68](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L68) +Defined in: [sub_cases/client.ts:68](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L68) ___ @@ -49,7 +49,7 @@ Retrieves the sub cases matching the search criteria. **Returns:** *Promise*<[*ISubCasesFindResponse*](typedoc_interfaces.isubcasesfindresponse.md)\> -Defined in: [sub_cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L72) +Defined in: [sub_cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L72) ___ @@ -67,7 +67,7 @@ Retrieves a single sub case. **Returns:** *Promise*<[*ISubCaseResponse*](typedoc_interfaces.isubcaseresponse.md)\> -Defined in: [sub_cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L76) +Defined in: [sub_cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L76) ___ @@ -86,4 +86,4 @@ Updates the specified sub cases to the new values included in the request. **Returns:** *Promise*<[*ISubCasesResponse*](typedoc_interfaces.isubcasesresponse.md)\> -Defined in: [sub_cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L80) +Defined in: [sub_cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L80) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md index f992a4116c800..5f0cc89239fd8 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md @@ -21,7 +21,7 @@ Parameters for retrieving user actions for a particular case The ID of the case -Defined in: [user_actions/client.ts:19](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/user_actions/client.ts#L19) +Defined in: [user_actions/client.ts:19](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/user_actions/client.ts#L19) ___ @@ -31,4 +31,4 @@ ___ If specified then a sub case will be used for finding all the user actions -Defined in: [user_actions/client.ts:23](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/user_actions/client.ts#L23) +Defined in: [user_actions/client.ts:23](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/user_actions/client.ts#L23) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md index e838a72159bef..df2641adf5a8c 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md @@ -28,4 +28,4 @@ Retrieves all user actions for a particular case. **Returns:** *Promise*<[*ICaseUserActionsResponse*](typedoc_interfaces.icaseuseractionsresponse.md)\> -Defined in: [user_actions/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/user_actions/client.ts#L33) +Defined in: [user_actions/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/user_actions/client.ts#L33) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md index acfa0b918aa9a..d4ca13501294a 100644 --- a/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md +++ b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md @@ -6,7 +6,7 @@ ### Interfaces -- [CaseIDsByAlertIDParams](../interfaces/cases_get.caseidsbyalertidparams.md) +- [CasesByAlertIDParams](../interfaces/cases_get.casesbyalertidparams.md) - [GetParams](../interfaces/cases_get.getparams.md) ### Functions @@ -31,7 +31,7 @@ Retrieves the reporters from all the cases. **Returns:** *Promise* -Defined in: [cases/get.ts:255](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L255) +Defined in: [cases/get.ts:290](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L290) ___ @@ -50,4 +50,4 @@ Retrieves the tags from all the cases. **Returns:** *Promise* -Defined in: [cases/get.ts:205](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L205) +Defined in: [cases/get.ts:240](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L240) diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 8a17ff9bd0ec1..0932308c2e269 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -12,6 +12,7 @@ import { User, AllTagsFindRequest, AllReportersFindRequest, + CasesByAlertId, } from '../../../common'; import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; @@ -28,9 +29,9 @@ import { create } from './create'; import { deleteCases } from './delete'; import { find } from './find'; import { - CaseIDsByAlertIDParams, + CasesByAlertIDParams, get, - getCaseIDsByAlertID, + getCasesByAlertID, GetParams, getReporters, getTags, @@ -79,9 +80,9 @@ export interface CasesSubClient { */ getReporters(params: AllReportersFindRequest): Promise; /** - * Retrieves the case IDs given a single alert ID + * Retrieves the cases ID and title that have the requested alert attached to them */ - getCaseIDsByAlertID(params: CaseIDsByAlertIDParams): Promise; + getCasesByAlertID(params: CasesByAlertIDParams): Promise; } /** @@ -103,8 +104,7 @@ export const createCasesSubClient = ( delete: (ids: string[]) => deleteCases(ids, clientArgs), getTags: (params: AllTagsFindRequest) => getTags(params, clientArgs), getReporters: (params: AllReportersFindRequest) => getReporters(params, clientArgs), - getCaseIDsByAlertID: (params: CaseIDsByAlertIDParams) => - getCaseIDsByAlertID(params, clientArgs), + getCasesByAlertID: (params: CasesByAlertIDParams) => getCasesByAlertID(params, clientArgs), }; return Object.freeze(casesSubClient); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index f908a8f091ef3..3df1891391c75 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -25,6 +25,8 @@ import { CasesByAlertIDRequest, CasesByAlertIDRequestRt, ENABLE_CASE_CONNECTOR, + CasesByAlertId, + CasesByAlertIdRt, } from '../../../common'; import { countAlertsForID, createCaseError, flattenCaseSavedObject } from '../../common'; import { CasesClientArgs } from '..'; @@ -35,7 +37,7 @@ import { CasesService } from '../../services'; /** * Parameters for finding cases IDs using an alert ID */ -export interface CaseIDsByAlertIDParams { +export interface CasesByAlertIDParams { /** * The alert ID to search for */ @@ -47,15 +49,15 @@ export interface CaseIDsByAlertIDParams { } /** - * Case Client wrapper function for retrieving the case IDs that have a particular alert ID + * Case Client wrapper function for retrieving the case IDs and titles that have a particular alert ID * attached to them. This handles RBAC before calling the saved object API. * * @ignore */ -export const getCaseIDsByAlertID = async ( - { alertID, options }: CaseIDsByAlertIDParams, +export const getCasesByAlertID = async ( + { alertID, options }: CasesByAlertIDParams, clientArgs: CasesClientArgs -): Promise => { +): Promise => { const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { @@ -75,12 +77,15 @@ export const getCaseIDsByAlertID = async ( Operations.getCaseIDsByAlertID.savedObjectType ); + // This will likely only return one comment saved object, the response aggregation will contain + // the keys we need to retrieve the cases const commentsWithAlert = await caseService.getCaseIdsByAlertId({ unsecuredSavedObjectsClient, alertId: alertID, filter, }); + // make sure the comments returned have the right owner ensureSavedObjectsAreAuthorized( commentsWithAlert.saved_objects.map((comment) => ({ owner: comment.attributes.owner, @@ -88,7 +93,37 @@ export const getCaseIDsByAlertID = async ( })) ); - return CasesService.getCaseIDsFromAlertAggs(commentsWithAlert); + const caseIds = CasesService.getCaseIDsFromAlertAggs(commentsWithAlert); + + // if we didn't find any case IDs then let's return early because there's nothing to request + if (caseIds.length <= 0) { + return []; + } + + const casesInfo = await caseService.getCases({ + unsecuredSavedObjectsClient, + caseIds, + }); + + // if there was an error retrieving one of the cases (maybe it was deleted, but the alert comment still existed) + // just ignore it + const validCasesInfo = casesInfo.saved_objects.filter( + (caseInfo) => caseInfo.error === undefined + ); + + ensureSavedObjectsAreAuthorized( + validCasesInfo.map((caseInfo) => ({ + owner: caseInfo.attributes.owner, + id: caseInfo.id, + })) + ); + + return CasesByAlertIdRt.encode( + validCasesInfo.map((caseInfo) => ({ + id: caseInfo.id, + title: caseInfo.attributes.title, + })) + ); } catch (error) { throw createCaseError({ message: `Failed to get case IDs using alert ID: ${alertID} options: ${JSON.stringify( diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index f6a36369c0b03..f7c27166ee910 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -28,7 +28,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => { delete: jest.fn(), getTags: jest.fn(), getReporters: jest.fn(), - getCaseIDsByAlertID: jest.fn(), + getCasesByAlertID: jest.fn(), }; }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts index f4b53a921ef88..3471c1dec6208 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../../types'; import { escapeHatch, wrapError } from '../../utils'; import { CASE_ALERTS_URL, CasesByAlertIDRequest } from '../../../../../common'; -export function initGetCaseIdsByAlertIdApi({ router, logger }: RouteDeps) { +export function initGetCasesByAlertIdApi({ router, logger }: RouteDeps) { router.get( { path: CASE_ALERTS_URL, @@ -33,7 +33,7 @@ export function initGetCaseIdsByAlertIdApi({ router, logger }: RouteDeps) { const options = request.query as CasesByAlertIDRequest; return response.ok({ - body: await casesClient.cases.getCaseIDsByAlertID({ alertID, options }), + body: await casesClient.cases.getCasesByAlertID({ alertID, options }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index 011464a73396f..266ea9ddb0f18 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -38,7 +38,7 @@ import { initPatchSubCasesApi } from './sub_case/patch_sub_cases'; import { initFindSubCasesApi } from './sub_case/find_sub_cases'; import { initDeleteSubCasesApi } from './sub_case/delete_sub_cases'; import { ENABLE_CASE_CONNECTOR } from '../../../common'; -import { initGetCaseIdsByAlertIdApi } from './cases/alerts/get_cases'; +import { initGetCasesByAlertIdApi } from './cases/alerts/get_cases'; import { initGetAllAlertsAttachToCaseApi } from './comments/get_alerts'; /** @@ -89,6 +89,6 @@ export function initCaseApi(deps: RouteDeps) { // Tags initGetTagsApi(deps); // Alerts - initGetCaseIdsByAlertIdApi(deps); + initGetCasesByAlertIdApi(deps); initGetAllAlertsAttachToCaseApi(deps); } diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx index 42d53f97d478b..ef311a7ca43b1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx @@ -41,27 +41,27 @@ export const HostIsolationPanel = React.memo( return findAlertId ? findAlertId[0] : ''; }, [details]); - const { caseIds } = useCasesFromAlerts({ alertId }); + const { casesInfo } = useCasesFromAlerts({ alertId }); // Cases related components to be used in both isolate and unisolate actions from the alert details flyout entry point - const caseCount: number = useMemo(() => caseIds.length, [caseIds]); + const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]); const casesList = useMemo( () => - caseIds.map((id, index) => { + casesInfo.map((caseInfo, index) => { return ( -
  • - +
  • +
  • ); }), - [caseIds] + [casesInfo] ); const associatedCases = useMemo(() => { @@ -90,7 +90,7 @@ export const HostIsolationPanel = React.memo( endpointId={endpointId} hostName={hostName} cases={associatedCases} - caseIds={caseIds} + casesInfo={casesInfo} cancelCallback={cancelCallback} /> ) : ( @@ -98,7 +98,7 @@ export const HostIsolationPanel = React.memo( endpointId={endpointId} hostName={hostName} cases={associatedCases} - caseIds={caseIds} + casesInfo={casesInfo} cancelCallback={cancelCallback} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx index afc2951e26e1f..b209c2f9c6e24 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx @@ -15,24 +15,29 @@ import { EndpointIsolateForm, EndpointIsolateSuccess, } from '../../../common/components/endpoint/host_isolation'; +import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types'; export const IsolateHost = React.memo( ({ endpointId, hostName, cases, - caseIds, + casesInfo, cancelCallback, }: { endpointId: string; hostName: string; cases: ReactNode; - caseIds: string[]; + casesInfo: CasesFromAlertsResponse; cancelCallback: () => void; }) => { const [comment, setComment] = useState(''); const [isIsolated, setIsIsolated] = useState(false); + const caseIds: string[] = casesInfo.map((caseInfo): string => { + return caseInfo.id; + }); + const { loading, isolateHost } = useHostIsolation({ endpointId, comment, caseIds }); const confirmHostIsolation = useCallback(async () => { @@ -47,7 +52,7 @@ export const IsolateHost = React.memo( [] ); - const caseCount: number = useMemo(() => caseIds.length, [caseIds]); + const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]); const hostIsolatedSuccess = useMemo(() => { return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx index 71f7cadda2f68..ad8e8eaddb39e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx @@ -15,24 +15,29 @@ import { EndpointUnisolateForm, } from '../../../common/components/endpoint/host_isolation'; import { useHostUnisolation } from '../../containers/detection_engine/alerts/use_host_unisolation'; +import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types'; export const UnisolateHost = React.memo( ({ endpointId, hostName, cases, - caseIds, + casesInfo, cancelCallback, }: { endpointId: string; hostName: string; cases: ReactNode; - caseIds: string[]; + casesInfo: CasesFromAlertsResponse; cancelCallback: () => void; }) => { const [comment, setComment] = useState(''); const [isUnIsolated, setIsUnIsolated] = useState(false); + const caseIds: string[] = casesInfo.map((caseInfo): string => { + return caseInfo.id; + }); + const { loading, unIsolateHost } = useHostUnisolation({ endpointId, comment, caseIds }); const confirmHostUnIsolation = useCallback(async () => { @@ -47,7 +52,7 @@ export const UnisolateHost = React.memo( [] ); - const caseCount: number = useMemo(() => caseIds.length, [caseIds]); + const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]); const hostUnisolatedSuccess = useMemo(() => { return ( diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts index 69358958a395c..e4bddfba8278b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts @@ -1046,6 +1046,6 @@ export const mockHostIsolation: HostIsolationResponse = { }; export const mockCaseIdsFromAlertId: CasesFromAlertsResponse = [ - '818601a0-b26b-11eb-8759-6b318e8cf4bc', - '8a774850-b26b-11eb-8759-6b318e8cf4bc', + { id: '818601a0-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 1' }, + { id: '8a774850-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 2' }, ]; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts index 52b477d95076b..54d4b6fdcbafd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts @@ -48,7 +48,7 @@ export interface AlertsIndex { index_mapping_outdated: boolean; } -export type CasesFromAlertsResponse = string[]; +export type CasesFromAlertsResponse = Array<{ id: string; title: string }>; export interface Privilege { username: string; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx index 0867fb001051a..00aa7c9baa9ac 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx @@ -35,7 +35,7 @@ describe('useCasesFromAlerts hook', () => { expect(spyOnCases).toHaveBeenCalledTimes(1); expect(result.current).toEqual({ loading: false, - caseIds: mockCaseIdsFromAlertId, + casesInfo: mockCaseIdsFromAlertId, }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx index 85b80a588e88d..eeb7968d6b2f2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx @@ -15,7 +15,7 @@ import { CasesFromAlertsResponse } from './types'; interface CasesFromAlertsStatus { loading: boolean; - caseIds: CasesFromAlertsResponse; + casesInfo: CasesFromAlertsResponse; } export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromAlertsStatus => { @@ -48,5 +48,5 @@ export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromA isMounted = false; }; }, [alertId, addError]); - return { loading, caseIds: cases }; + return { loading, casesInfo: cases }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 1424d5146950f..50fe2ffe2cea9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -9,6 +9,8 @@ import moment from 'moment'; import { RequestHandler } from 'src/core/server'; import uuid from 'uuid'; import { TypeOf } from '@kbn/config-schema'; +import { CommentType } from '../../../../../cases/common'; +import { CasesByAlertId } from '../../../../../cases/common/api/cases/case'; import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions'; import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; @@ -20,7 +22,6 @@ import { import { getAgentIDsForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; import { APP_ID } from '../../../../common/constants'; -import { CommentType } from '../../../../../cases/common'; import { userCanIsolate } from '../../../../common/endpoint/actions'; /** @@ -103,12 +104,17 @@ export const isolationRequestHandler = function ( let caseIDs: string[] = req.body.case_ids?.slice() || []; if (req.body.alert_ids && req.body.alert_ids.length > 0) { const newIDs: string[][] = await Promise.all( - req.body.alert_ids.map(async (a: string) => - (await endpointContext.service.getCasesClient(req)).cases.getCaseIDsByAlertID({ + req.body.alert_ids.map(async (a: string) => { + const cases: CasesByAlertId = await ( + await endpointContext.service.getCasesClient(req) + ).cases.getCasesByAlertID({ alertID: a, options: { owner: APP_ID }, - }) - ) + }); + return cases.map((caseInfo): string => { + return caseInfo.id; + }); + }) ); caseIDs = caseIDs.concat(...newIDs); } 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 63be1736405fc..921589b2341dd 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -46,6 +46,7 @@ import { CasesConfigurationsResponse, CaseUserActionsResponse, AlertResponse, + CasesByAlertId, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; @@ -1017,7 +1018,7 @@ export const findCases = async ({ return res; }; -export const getCaseIDsByAlert = async ({ +export const getCasesByAlert = async ({ supertest, alertID, query = {}, @@ -1029,7 +1030,7 @@ export const getCaseIDsByAlert = async ({ query?: Record; expectedHttpCode?: number; auth?: { user: User; space: string | null }; -}): Promise => { +}): Promise => { const { body: res } = await supertest .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/alerts/${alertID}`) .auth(auth.user.username, auth.user.password) diff --git a/x-pack/test/case_api_integration/common/lib/validation.ts b/x-pack/test/case_api_integration/common/lib/validation.ts new file mode 100644 index 0000000000000..8b1c8ca124149 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/validation.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { CaseResponse, CasesByAlertId } from '../../../../plugins/cases/common'; + +/** + * Ensure that the result of the alerts API request matches with the cases created for the test. + */ +export function validateCasesFromAlertIDResponse( + casesFromAPIResponse: CasesByAlertId, + createdCasesForTest: CaseResponse[] +) { + const idToTitle = new Map( + createdCasesForTest.map((caseInfo) => [caseInfo.id, caseInfo.title]) + ); + + for (const apiResCase of casesFromAPIResponse) { + // check that the title in the api response matches the title in the map from the created cases + expect(apiResCase.title).to.be(idToTitle.get(apiResCase.id)); + } +} diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts index e34f879e3aff8..136e52d08f46a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts @@ -13,9 +13,10 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/ import { createCase, createComment, - getCaseIDsByAlert, + getCasesByAlert, deleteAllCaseItems, } from '../../../../common/lib/utils'; +import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation'; import { CaseResponse } from '../../../../../../plugins/cases/common'; import { globalRead, @@ -41,9 +42,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should return all cases with the same alert ID attached to them', async () => { const [case1, case2, case3] = await Promise.all([ - createCase(supertest, getPostCaseRequest()), - createCase(supertest, getPostCaseRequest()), - createCase(supertest, getPostCaseRequest()), + createCase(supertest, getPostCaseRequest({ title: 'a' })), + createCase(supertest, getPostCaseRequest({ title: 'b' })), + createCase(supertest, getPostCaseRequest({ title: 'c' })), ]); await Promise.all([ @@ -52,12 +53,10 @@ export default ({ getService }: FtrProviderContext): void => { createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }), ]); - const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id' }); + const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id' }); expect(caseIDsWithAlert.length).to.eql(3); - expect(caseIDsWithAlert).to.contain(case1.id); - expect(caseIDsWithAlert).to.contain(case2.id); - expect(caseIDsWithAlert).to.contain(case3.id); + validateCasesFromAlertIDResponse(caseIDsWithAlert, [case1, case2, case3]); }); it('should return all cases with the same alert ID when more than 100 cases', async () => { @@ -80,13 +79,11 @@ export default ({ getService }: FtrProviderContext): void => { await Promise.all(commentPromises); - const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id' }); + const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id' }); expect(caseIDsWithAlert.length).to.eql(numCases); - for (const caseInfo of cases) { - expect(caseIDsWithAlert).to.contain(caseInfo.id); - } + validateCasesFromAlertIDResponse(caseIDsWithAlert, cases); }); it('should return no cases when the alert ID is not found', async () => { @@ -102,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }), ]); - const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id100' }); + const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id100' }); expect(caseIDsWithAlert.length).to.eql(0); }); @@ -120,7 +117,7 @@ export default ({ getService }: FtrProviderContext): void => { createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }), ]); - const caseIDsWithAlert = await getCaseIDsByAlert({ + const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id', query: { owner: 'not-real' }, @@ -137,7 +134,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('rbac', () => { const supertestWithoutAuth = getService('supertestWithoutAuth'); - it('should return the correct case IDs', async () => { + it('should return the correct cases info', async () => { const secOnlyAuth = { user: secOnly, space: 'space1' }; const obsOnlyAuth = { user: obsOnly, space: 'space1' }; @@ -176,20 +173,20 @@ export default ({ getService }: FtrProviderContext): void => { for (const scenario of [ { user: globalRead, - caseIDs: [case1.id, case2.id, case3.id], + cases: [case1, case2, case3], }, { user: superUser, - caseIDs: [case1.id, case2.id, case3.id], + cases: [case1, case2, case3], }, - { user: secOnlyRead, caseIDs: [case1.id, case2.id] }, - { user: obsOnlyRead, caseIDs: [case3.id] }, + { user: secOnlyRead, cases: [case1, case2] }, + { user: obsOnlyRead, cases: [case3] }, { user: obsSecRead, - caseIDs: [case1.id, case2.id, case3.id], + cases: [case1, case2, case3], }, ]) { - const res = await getCaseIDsByAlert({ + const res = await getCasesByAlert({ supertest: supertestWithoutAuth, // cast because the official type is string | string[] but the ids will always be a single value in the tests alertID: postCommentAlertReq.alertId as string, @@ -198,10 +195,9 @@ export default ({ getService }: FtrProviderContext): void => { space: 'space1', }, }); - expect(res.length).to.eql(scenario.caseIDs.length); - for (const caseID of scenario.caseIDs) { - expect(res).to.contain(caseID); - } + expect(res.length).to.eql(scenario.cases.length); + + validateCasesFromAlertIDResponse(res, scenario.cases); } }); @@ -224,7 +220,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: { user: superUser, space: scenario.space }, }); - await getCaseIDsByAlert({ + await getCasesByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth: { user: scenario.user, space: scenario.space }, @@ -260,17 +256,17 @@ export default ({ getService }: FtrProviderContext): void => { }), ]); - const res = await getCaseIDsByAlert({ + const res = await getCasesByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth, query: { owner: 'securitySolutionFixture' }, }); - expect(res).to.eql([case1.id]); + expect(res).to.eql([{ id: case1.id, title: case1.title }]); }); - it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => { + it('should return the correct cases info when the owner query parameter contains unprivileged values', async () => { const auth = { user: obsSec, space: 'space1' }; const [case1, case2] = await Promise.all([ createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth), @@ -297,7 +293,7 @@ export default ({ getService }: FtrProviderContext): void => { }), ]); - const res = await getCaseIDsByAlert({ + const res = await getCasesByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth: { user: secOnly, space: 'space1' }, @@ -305,7 +301,7 @@ export default ({ getService }: FtrProviderContext): void => { query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); - expect(res).to.eql([case1.id]); + expect(res).to.eql([{ id: case1.id, title: case1.title }]); }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts index 9575bd99112f6..f55427d13b32b 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -12,7 +12,7 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/ import { createCase, createComment, - getCaseIDsByAlert, + getCasesByAlert, deleteAllCaseItems, } from '../../../../common/lib/utils'; import { @@ -30,6 +30,7 @@ import { superUserDefaultSpaceAuth, obsSecDefaultSpaceAuth, } from '../../../utils'; +import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -43,7 +44,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); - it('should return the correct case IDs', async () => { + it('should return the correct cases info', async () => { const [case1, case2, case3] = await Promise.all([ createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), @@ -79,20 +80,20 @@ export default ({ getService }: FtrProviderContext): void => { for (const scenario of [ { user: globalRead, - caseIDs: [case1.id, case2.id, case3.id], + cases: [case1, case2, case3], }, { user: superUser, - caseIDs: [case1.id, case2.id, case3.id], + cases: [case1, case2, case3], }, - { user: secOnlyReadSpacesAll, caseIDs: [case1.id, case2.id] }, - { user: obsOnlyReadSpacesAll, caseIDs: [case3.id] }, + { user: secOnlyReadSpacesAll, cases: [case1, case2] }, + { user: obsOnlyReadSpacesAll, cases: [case3] }, { user: obsSecReadSpacesAll, - caseIDs: [case1.id, case2.id, case3.id], + cases: [case1, case2, case3], }, ]) { - const res = await getCaseIDsByAlert({ + const cases = await getCasesByAlert({ supertest: supertestWithoutAuth, // cast because the official type is string | string[] but the ids will always be a single value in the tests alertID: postCommentAlertReq.alertId as string, @@ -101,10 +102,9 @@ export default ({ getService }: FtrProviderContext): void => { space: null, }, }); - expect(res.length).to.eql(scenario.caseIDs.length); - for (const caseID of scenario.caseIDs) { - expect(res).to.contain(caseID); - } + + expect(cases.length).to.eql(scenario.cases.length); + validateCasesFromAlertIDResponse(cases, scenario.cases); } }); @@ -123,7 +123,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: superUserDefaultSpaceAuth, }); - await getCaseIDsByAlert({ + await getCasesByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth: { user: noKibanaPrivileges, space: null }, @@ -157,7 +157,7 @@ export default ({ getService }: FtrProviderContext): void => { }), ]); - await getCaseIDsByAlert({ + await getCasesByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth: { user: obsSecSpacesAll, space: 'space1' }, @@ -192,17 +192,17 @@ export default ({ getService }: FtrProviderContext): void => { }), ]); - const res = await getCaseIDsByAlert({ + const cases = await getCasesByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth: obsSecDefaultSpaceAuth, query: { owner: 'securitySolutionFixture' }, }); - expect(res).to.eql([case1.id]); + expect(cases).to.eql([{ id: case1.id, title: case1.title }]); }); - it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => { + it('should return the correct cases info when the owner query parameter contains unprivileged values', async () => { const [case1, case2] = await Promise.all([ createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), createCase( @@ -228,7 +228,7 @@ export default ({ getService }: FtrProviderContext): void => { }), ]); - const res = await getCaseIDsByAlert({ + const cases = await getCasesByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth: secOnlyDefaultSpaceAuth, @@ -236,7 +236,7 @@ export default ({ getService }: FtrProviderContext): void => { query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); - expect(res).to.eql([case1.id]); + expect(cases).to.eql([{ id: case1.id, title: case1.title }]); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts index 9587502fb642c..739f8e5ec0892 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts @@ -12,10 +12,11 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/ import { createCase, createComment, - getCaseIDsByAlert, + getCasesByAlert, deleteAllCaseItems, getAuthWithSuperUser, } from '../../../../common/lib/utils'; +import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -57,16 +58,14 @@ export default ({ getService }: FtrProviderContext): void => { }), ]); - const caseIDsWithAlert = await getCaseIDsByAlert({ + const cases = await getCasesByAlert({ supertest, alertID: 'test-id', auth: authSpace1, }); - expect(caseIDsWithAlert.length).to.eql(3); - expect(caseIDsWithAlert).to.contain(case1.id); - expect(caseIDsWithAlert).to.contain(case2.id); - expect(caseIDsWithAlert).to.contain(case3.id); + expect(cases.length).to.eql(3); + validateCasesFromAlertIDResponse(cases, [case1, case2, case3]); }); it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => { @@ -97,14 +96,14 @@ export default ({ getService }: FtrProviderContext): void => { }), ]); - const caseIDsWithAlert = await getCaseIDsByAlert({ + const casesByAlert = await getCasesByAlert({ supertest, alertID: 'test-id', auth: authSpace2, }); - expect(caseIDsWithAlert.length).to.eql(1); - expect(caseIDsWithAlert).to.eql([case3.id]); + expect(casesByAlert.length).to.eql(1); + expect(casesByAlert).to.eql([{ id: case3.id, title: case3.title }]); }); }); }; From 8792ca1b13ef4caaa345b661417f130deec0d5ea Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 22 Jun 2021 13:28:09 -0700 Subject: [PATCH 074/118] [Monitoring] Update Kibana rules/alerts language in setup mode (#102441) (#102979) --- x-pack/plugins/monitoring/public/alerts/badge.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 8b4075ba67cdc..44af8b3327975 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -19,13 +19,18 @@ import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category'; import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node'; export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`; +export const numberOfRulesLabel = (count: number) => `${count} rule${count > 1 ? 's' : ''}`; const MAX_TO_SHOW_BY_CATEGORY = 8; -const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', { +const PANEL_TITLE_ALERTS = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', { defaultMessage: 'Alerts', }); +const PANEL_TITLE_RULES = i18n.translate('xpack.monitoring.rules.badge.panelTitle', { + defaultMessage: 'Rules', +}); + const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', { defaultMessage: 'Group by node', }); @@ -54,6 +59,7 @@ export const AlertsBadge: React.FC = (props: Props) => { const [showByNode, setShowByNode] = React.useState( !inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY ); + const PANEL_TITLE = inSetupMode ? PANEL_TITLE_RULES : PANEL_TITLE_ALERTS; React.useEffect(() => { if (inSetupMode && showByNode) { @@ -93,10 +99,12 @@ export const AlertsBadge: React.FC = (props: Props) => { setShowPopover(true)} > - {numberOfAlertsLabel(alertCount)} + {inSetupMode ? numberOfRulesLabel(alertCount) : numberOfAlertsLabel(alertCount)} ); From c5780aeb14829c4c3a6e4b516366b0b9ea3dc34d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:34:22 -0400 Subject: [PATCH 075/118] [DOCS] Updating Elastic Security Overview topic (#101922) (#103006) * updating overview topic for Kibana * formatting fixes * small formatting tweaks * small formatting tweaks Co-authored-by: Janeen Mikell-Straughn <57149392+jmikell821@users.noreply.github.com> --- docs/siem/images/workflow.png | Bin 0 -> 308403 bytes docs/siem/siem-ui.asciidoc | 238 +++++++++++++++++++++------------- 2 files changed, 148 insertions(+), 90 deletions(-) create mode 100644 docs/siem/images/workflow.png diff --git a/docs/siem/images/workflow.png b/docs/siem/images/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..b71c7b0ace301e3554d71f4d2f156bbe476cbf10 GIT binary patch literal 308403 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%0A_ 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 z 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.11.0, {elastic-sec} supports cold tier data for the following {es} indices: + +* Index patterns specified in `securitySolution:defaultIndex` +* Index patterns specified in the definitions of detection rules, except for indicator match rules +* Index patterns specified in the data sources selector on various {security-app} pages + +{elastic-sec} does NOT support cold tier data for the following {es} indices: + +* Index patterns controlled by {elastic-sec}, including signals and list indices +* Index patterns specified in indicator match rules + +Using cold tier data for unsupported indices may result in detection rule timeouts and overall performance degradation. + +[discrete] +[[self-protection]] +==== Elastic Endpoint self-protection + +Self-protection means that {elastic-endpoint} has guards against users and attackers that may try to interfere with its functionality. This protection feature is consistently enhanced to prevent attackers who may attempt to use newer, more sophisticated tactics to interfere with the {elastic-endpoint}. Self-protection is enabled by default when {elastic-endpoint} installs on supported platforms, listed below. + +Self-protection is enabled on the following 64-bit Windows versions: + +* Windows 8.1 +* Windows 10 +* Windows Server 2012 R2 +* Windows Server 2016 +* Windows Server 2019 + +And on the following macOS versions: + +* macOS 10.15 (Catalina) +* macOS 11 (Big Sur) + +NOTE: Other Windows and macOS variants (and all Linux distributions) do not have self-protection. + +For {stack} version >= 7.11.0, self-protection defines the following permissions: + +* Users -- even Administrator/root -- *cannot* delete {elastic-endpoint} files (located at `c:\Program Files\Elastic\Endpoint` on Windows, and `/Library/Elastic/Endpoint` on macOS). +* Users *cannot* terminate the {elastic-endpoint} program or service. +* Administrator/root users *can* read the endpoint's files. On Windows, the easiest way to read Endpoint files is to start an Administrator `cmd.exe` prompt. On macOS, an Administrator can use the `sudo` command. +* Administrator/root users *can* stop the {elastic-agent}'s service. On Windows, run the `sc stop "Elastic Agent"` command. On macOS, run the `sudo launchctl stop elastic-agent` command. + + +[discrete] +[[siem-integration]] +=== Integration with other Elastic products + +You can use {elastic-sec} with other Elastic products and features to help you +identify and investigate suspicious activity: + +* https://www.elastic.co/products/stack/machine-learning[{ml-cap}] +* https://www.elastic.co/products/stack/alerting[Alerting] +* https://www.elastic.co/products/stack/canvas[Canvas] + +[discrete] +[[data-sources]] +=== APM transaction data sources + +By default, {elastic-sec} monitors {apm-app-ref}/apm-getting-started.html[APM] +`apm-*-transaction*` indices. To add additional APM indices, update the +index patterns in the `securitySolution:defaultIndex` setting ({kib} -> Stack Management -> Advanced Settings -> `securitySolution:defaultIndex`). +[discrete] +[[ecs-compliant-reqs]] +=== ECS compliance data requirements +The {ecs-ref}[Elastic Common Schema (ECS)] defines a common set of fields to be used for +storing event data in Elasticsearch. ECS helps users normalize their event data +to better analyze, visualize, and correlate the data represented in their +events. {elastic-sec} supports events and indicator index data from any ECS-compliant data source. +IMPORTANT: {elastic-sec} requires {ecs-ref}[ECS-compliant data]. If you use third-party data collectors to ship data to {es}, the data must be mapped to ECS. +{security-guide}/siem-field-reference.html[Elastic Security ECS field reference] lists ECS fields used in {elastic-sec}. From cb684f5a800e8f10a7800e26befe46bf5d589178 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:41:36 -0400 Subject: [PATCH 076/118] Allow initialNamespaces to be used for isolated types (#102585) (#102985) Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- docs/api/saved-objects/bulk_create.asciidoc | 5 + docs/api/saved-objects/create.asciidoc | 5 + ...jectsbulkcreateobject.initialnamespaces.md | 2 +- ...ore-server.savedobjectsbulkcreateobject.md | 2 +- ...dobjectscreateoptions.initialnamespaces.md | 2 +- ...n-core-server.savedobjectscreateoptions.md | 2 +- .../service/lib/repository.test.js | 145 +++++++++++++----- .../saved_objects/service/lib/repository.ts | 74 +++++---- .../service/saved_objects_client.ts | 12 +- src/core/server/server.api.md | 2 +- .../common/lib/saved_object_test_utils.ts | 6 +- .../common/suites/bulk_create.ts | 22 ++- .../common/suites/create.ts | 22 ++- .../security_and_spaces/apis/bulk_create.ts | 19 ++- .../security_and_spaces/apis/create.ts | 19 ++- .../security_only/apis/bulk_create.ts | 18 ++- .../security_only/apis/create.ts | 18 ++- .../spaces_only/apis/bulk_create.ts | 18 ++- .../spaces_only/apis/create.ts | 18 ++- 19 files changed, 307 insertions(+), 104 deletions(-) diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc index d54a14224fc50..70398a851055c 100644 --- a/docs/api/saved-objects/bulk_create.asciidoc +++ b/docs/api/saved-objects/bulk_create.asciidoc @@ -44,6 +44,11 @@ experimental[] Create multiple {kib} saved objects. (Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space (default behavior). +* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including +the "All spaces" identifier (`'*'`). +* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be +used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. +* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. `version`:: (Optional, number) Specifies the version. diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index 0ea40e6565815..cc66d6236ff57 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -53,6 +53,11 @@ any data that you send to the API is properly formed. (Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space (default behavior). +* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including +the "All spaces" identifier (`'*'`). +* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be +used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. +* For global object types (registered with `namespaceType: 'agnostic'): this option cannot be used. [[saved-objects-api-create-request-codes]] ==== Response code diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md index 3db8bbadfbd6b..4d094ecde7a96 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md @@ -6,7 +6,7 @@ Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md). -Note: this can only be used for multi-namespace object types. +\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md index 6fc01212a2e41..463c3fe81b702 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md @@ -18,7 +18,7 @@ export interface SavedObjectsBulkCreateObject | [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T | | | [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | | [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string | | -| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | +| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | | [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md index 262b0997cb905..43489b8d2e8a2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md @@ -6,7 +6,7 @@ Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md). -Note: this can only be used for multi-namespace object types. +\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md index 1805f389d4e7f..7eaa9c51f5c82 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md @@ -17,7 +17,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | --- | --- | --- | | [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | | [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | -| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | +| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | | [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 22c40a547f419..4456784fdbc0b 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -525,15 +525,22 @@ describe('SavedObjectsRepository', () => { const ns2 = 'bar-namespace'; const ns3 = 'baz-namespace'; const objects = [ - { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] }, - { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] }, + { ...obj1, type: 'dashboard', initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] }, ]; await bulkCreateSuccess(objects, { namespace, overwrite: true }); const body = [ - expect.any(Object), + { index: expect.objectContaining({ _id: `${ns2}:dashboard:${obj1.id}` }) }, + expect.objectContaining({ namespace: ns2 }), + { + index: expect.objectContaining({ + _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj1.id}`, + }), + }, expect.objectContaining({ namespaces: [ns2] }), - expect.any(Object), - expect.objectContaining({ namespaces: [ns3] }), + { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj1.id}` }) }, + expect.objectContaining({ namespaces: [ns2, ns3] }), ]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), @@ -649,24 +656,19 @@ describe('SavedObjectsRepository', () => { ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); }); - it(`returns error when initialNamespaces is used with a non-shareable object`, async () => { - const test = async (objType) => { - const obj = { ...obj3, type: objType, initialNamespaces: [] }; - await bulkCreateError( + it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => { + const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( obj, - undefined, - expectErrorResult( - obj, - createBadRequestError('"initialNamespaces" can only be used on multi-namespace types') - ) - ); - }; - await test('dashboard'); - await test(NAMESPACE_AGNOSTIC_TYPE); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); + createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') + ) + ); }); - it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => { + it(`returns error when initialNamespaces is empty`, async () => { const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] }; await bulkCreateError( obj, @@ -678,6 +680,26 @@ describe('SavedObjectsRepository', () => { ); }); + it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType, initialNamespaces) => { + const obj = { ...obj3, type: objType, initialNamespaces }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ) + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + it(`returns error when type is invalid`, async () => { const obj = { ...obj3, type: 'unknownType' }; await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); @@ -1865,12 +1887,46 @@ describe('SavedObjectsRepository', () => { }); it(`adds initialNamespaces instead of namespace`, async () => { - const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] }; - await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options); - expect(client.create).toHaveBeenCalledWith( + const ns2 = 'bar-namespace'; + const ns3 = 'baz-namespace'; + await savedObjectsRepository.create('dashboard', attributes, { + id, + namespace, + initialNamespaces: [ns2], + }); + await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { + id, + namespace, + initialNamespaces: [ns2], + }); + await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { + id, + namespace, + initialNamespaces: [ns2, ns3], + }); + + expect(client.create).toHaveBeenCalledTimes(3); + expect(client.create).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + id: `${ns2}:dashboard:${id}`, + body: expect.objectContaining({ namespace: ns2 }), + }), + expect.anything() + ); + expect(client.create).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, + body: expect.objectContaining({ namespaces: [ns2] }), + }), + expect.anything() + ); + expect(client.create).toHaveBeenNthCalledWith( + 3, expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: options.initialNamespaces }), + body: expect.objectContaining({ namespaces: [ns2, ns3] }), }), expect.anything() ); @@ -1892,29 +1948,40 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { - it(`throws when options.initialNamespaces is used with a non-shareable object`, async () => { - const test = async (objType) => { - await expect( - savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] }) - ).rejects.toThrowError( - createBadRequestError( - '"options.initialNamespaces" can only be used on multi-namespace types' - ) - ); - }; - await test('dashboard'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); + it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => { + await expect( + savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, { + initialNamespaces: [namespace], + }) + ).rejects.toThrowError( + createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') + ); }); - it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => { + it(`throws when options.initialNamespaces is empty`, async () => { await expect( savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] }) ).rejects.toThrowError( - createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings') + createBadRequestError('"initialNamespaces" must be a non-empty array of strings') ); }); + it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType, initialNamespaces) => { + await expect( + savedObjectsRepository.create(objType, attributes, { initialNamespaces }) + ).rejects.toThrowError( + createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + it(`throws when options.namespace is '*'`, async () => { await expect( savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING }) diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 1577f773434b9..c9fa50da55df1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -283,28 +283,18 @@ export class SavedObjectsRepository { } = options; const namespace = normalizeNamespace(options.namespace); - if (initialNamespaces) { - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"options.initialNamespaces" can only be used on multi-namespace types' - ); - } else if (!initialNamespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"options.initialNamespaces" must be a non-empty array of strings' - ); - } - } + this.validateInitialNamespaces(type, initialNamespaces); if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } const time = this._getCurrentTime(); - let savedObjectNamespace; + let savedObjectNamespace: string | undefined; let savedObjectNamespaces: string[] | undefined; - if (this._registry.isSingleNamespace(type) && namespace) { - savedObjectNamespace = namespace; + if (this._registry.isSingleNamespace(type)) { + savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace; } else if (this._registry.isMultiNamespace(type)) { if (id && overwrite) { // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces @@ -369,32 +359,29 @@ export class SavedObjectsRepository { let bulkGetRequestIndexCounter = 0; const expectedResults: Either[] = objects.map((object) => { + const { type, id, initialNamespaces } = object; let error: DecoratedError | undefined; - if (!this._allowedTypes.includes(object.type)) { - error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type); - } else if (object.initialNamespaces) { - if (!this._registry.isShareable(object.type)) { - error = SavedObjectsErrorHelpers.createBadRequestError( - '"initialNamespaces" can only be used on multi-namespace types' - ); - } else if (!object.initialNamespaces.length) { - error = SavedObjectsErrorHelpers.createBadRequestError( - '"initialNamespaces" must be a non-empty array of strings' - ); + if (!this._allowedTypes.includes(type)) { + error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } else { + try { + this.validateInitialNamespaces(type, initialNamespaces); + } catch (e) { + error = e; } } if (error) { return { tag: 'Left' as 'Left', - error: { id: object.id, type: object.type, error: errorContent(error) }, + error: { id, type, error: errorContent(error) }, }; } - const method = object.id && overwrite ? 'index' : 'create'; - const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type); + const method = id && overwrite ? 'index' : 'create'; + const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type); - if (object.id == null) { + if (id == null) { object.id = SavedObjectsUtils.generateId(); } @@ -434,8 +421,8 @@ export class SavedObjectsRepository { return expectedBulkGetResult; } - let savedObjectNamespace; - let savedObjectNamespaces; + let savedObjectNamespace: string | undefined; + let savedObjectNamespaces: string[] | undefined; let versionProperties; const { esRequestIndex, @@ -469,7 +456,7 @@ export class SavedObjectsRepository { versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(object.type)) { - savedObjectNamespace = namespace; + savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace; } else if (this._registry.isMultiNamespace(object.type)) { savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); } @@ -2080,6 +2067,29 @@ export class SavedObjectsRepository { const object = await this.get(type, id, options); return { saved_object: object, outcome: 'exactMatch' }; } + + private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) { + if (!initialNamespaces) { + return; + } + + if (this._registry.isNamespaceAgnostic(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" cannot be used on space-agnostic types' + ); + } else if (!initialNamespaces.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" must be a non-empty array of strings' + ); + } else if ( + !this._registry.isShareable(type) && + (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING)) + ) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ); + } + } } /** diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index af682cfb81296..1423050145695 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -63,7 +63,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in * {@link SavedObjectsCreateOptions}. * - * Note: this can only be used for multi-namespace object types. + * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, + * including the "All spaces" identifier (`'*'`). + * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only + * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. + * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. */ initialNamespaces?: string[]; } @@ -96,7 +100,11 @@ export interface SavedObjectsBulkCreateObject { * Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in * {@link SavedObjectsCreateOptions}. * - * Note: this can only be used for multi-namespace object types. + * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, + * including the "All spaces" identifier (`'*'`). + * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only + * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. + * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. */ initialNamespaces?: string[]; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9e7721fde90e7..fcecf39f7e53a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2901,7 +2901,7 @@ export class SavedObjectsRepository { resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; -} + } // @public export interface SavedObjectsRepositoryFactory { diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts index b712c2882ee0f..eb0c161049cf0 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -154,12 +154,14 @@ export const expectResponses = { // bulk request error expect(object.type).to.eql(type); expect(object.id).to.eql(id); - expect(object.error).to.eql(error.output.payload); + expect(object.error.error).to.eql(error.output.payload.error); + expect(object.error.statusCode).to.eql(error.output.payload.statusCode); + // ignore the error.message, because it can vary for decorated errors } else { // non-bulk request error expect(object.error).to.eql(error.output.payload.error); expect(object.statusCode).to.eql(error.output.payload.statusCode); - // ignore the error.message, because it can vary for decorated non-bulk errors (e.g., conflict) + // ignore the error.message, because it can vary for decorated errors } } else { // fall back to default behavior of testing the success outcome diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index 5860ec1f193b2..06758da1ebad2 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -41,13 +41,25 @@ const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); -const NEW_EACH_SPACE_OBJ = Object.freeze({ +const INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE = Object.freeze({ + type: 'isolatedtype', + id: 'new-other-space-id', + expectedNamespaces: ['other-space'], // expected namespaces of resulting object + initialNamespaces: ['other-space'], // args passed to the bulkCreate method +}); +const INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE = Object.freeze({ + type: 'sharecapabletype', + id: 'new-other-space-id', + expectedNamespaces: ['other-space'], // expected namespaces of resulting object + initialNamespaces: ['other-space'], // args passed to the bulkCreate method +}); +const INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE = Object.freeze({ type: 'sharedtype', id: 'new-each-space-id', expectedNamespaces: EACH_SPACE, // expected namespaces of resulting object initialNamespaces: EACH_SPACE, // args passed to the bulkCreate method }); -const NEW_ALL_SPACES_OBJ = Object.freeze({ +const INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES = Object.freeze({ type: 'sharedtype', id: 'new-all-spaces-id', expectedNamespaces: [ALL_SPACES_ID], // expected namespaces of resulting object @@ -58,8 +70,10 @@ export const TEST_CASES: Record = Object.freeze({ ...CASES, NEW_SINGLE_NAMESPACE_OBJ, NEW_MULTI_NAMESPACE_OBJ, - NEW_EACH_SPACE_OBJ, - NEW_ALL_SPACES_OBJ, + INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, NEW_NAMESPACE_AGNOSTIC_OBJ, }); diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts index ff2bfdefb4c08..298e1a9807175 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -41,13 +41,25 @@ const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; // we could create six separate test cases to test every permutation, but there's no real value in doing so const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: '' }); const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); -const NEW_EACH_SPACE_OBJ = Object.freeze({ +const INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE = Object.freeze({ + type: 'isolatedtype', + id: 'new-other-space-id', + expectedNamespaces: ['other-space'], // expected namespaces of resulting object + initialNamespaces: ['other-space'], // args passed to the bulkCreate method +}); +const INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE = Object.freeze({ + type: 'sharecapabletype', + id: 'new-other-space-id', + expectedNamespaces: ['other-space'], // expected namespaces of resulting object + initialNamespaces: ['other-space'], // args passed to the bulkCreate method +}); +const INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE = Object.freeze({ type: 'sharedtype', id: 'new-each-space-id', expectedNamespaces: EACH_SPACE, // expected namespaces of resulting object initialNamespaces: EACH_SPACE, // args passed to the bulkCreate method }); -const NEW_ALL_SPACES_OBJ = Object.freeze({ +const INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES = Object.freeze({ type: 'sharedtype', id: 'new-all-spaces-id', expectedNamespaces: [ALL_SPACES_ID], // expected namespaces of resulting object @@ -58,8 +70,10 @@ export const TEST_CASES: Record = Object.freeze({ ...CASES, NEW_SINGLE_NAMESPACE_OBJ, NEW_MULTI_NAMESPACE_OBJ, - NEW_EACH_SPACE_OBJ, - NEW_ALL_SPACES_OBJ, + INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, NEW_NAMESPACE_AGNOSTIC_OBJ, }); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index 1fa24c6d6e2d6..e048a4abc8ccc 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; +import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -75,7 +75,22 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const crossNamespace = [CASES.NEW_EACH_SPACE_OBJ, CASES.NEW_ALL_SPACES_OBJ]; + const crossNamespace = [ + { + ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + initialNamespaces: ['x', 'y'], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + { + ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + initialNamespaces: [ALL_SPACES_ID], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, + ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; const allTypes = normalTypes.concat(hiddenType); return { normalTypes, crossNamespace, hiddenType, allTypes }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts index 3553ae0e5b538..8215c991a9287 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; +import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -62,7 +62,22 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; - const crossNamespace = [CASES.NEW_EACH_SPACE_OBJ, CASES.NEW_ALL_SPACES_OBJ]; + const crossNamespace = [ + { + ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + initialNamespaces: ['x', 'y'], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + { + ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + initialNamespaces: [ALL_SPACES_ID], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, + ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; const allTypes = normalTypes.concat(crossNamespace, hiddenType); return { normalTypes, crossNamespace, hiddenType, allTypes }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 7487466f4b38c..f9423d77c5bb5 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; +import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -39,8 +39,20 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - CASES.NEW_EACH_SPACE_OBJ, - CASES.NEW_ALL_SPACES_OBJ, + { + ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + initialNamespaces: ['x', 'y'], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + { + ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + initialNamespaces: [ALL_SPACES_ID], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; const allTypes = normalTypes.concat(hiddenType); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts index 7eda7f5283448..67195637f0c0a 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; +import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -38,8 +38,20 @@ const createTestCases = (overwrite: boolean) => { { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - CASES.NEW_EACH_SPACE_OBJ, - CASES.NEW_ALL_SPACES_OBJ, + { + ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + initialNamespaces: ['x', 'y'], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + { + ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + initialNamespaces: [ALL_SPACES_ID], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; const allTypes = normalTypes.concat(hiddenType); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index 5812aaf43060d..c448d73ce7bf8 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; +import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { bulkCreateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_create'; @@ -70,8 +70,20 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - CASES.NEW_EACH_SPACE_OBJ, - CASES.NEW_ALL_SPACES_OBJ, + { + ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + initialNamespaces: ['x', 'y'], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + { + ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + initialNamespaces: [ALL_SPACES_ID], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, ]; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts index 4c91781b6ab2c..7c8726896c18a 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SPACES } from '../../common/lib/spaces'; +import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/create'; @@ -57,8 +57,20 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, - CASES.NEW_EACH_SPACE_OBJ, - CASES.NEW_ALL_SPACES_OBJ, + { + ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, + initialNamespaces: ['x', 'y'], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + { + ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, + initialNamespaces: [ALL_SPACES_ID], + ...fail400(), // cannot be created in multiple spaces + }, + CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE, + CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES, ]; }; From c2357faddfaef83e10e4e3fc49a482e658f12415 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:44:35 -0400 Subject: [PATCH 077/118] [App Search] Convert API Logs page to new page template + empty state polish (#102820) (#102987) * Convert API Logs noItemsMessage to its own empty state prompt - Will be used by new page template * Convert API Logs view to new page template + use new empty state + add tests clarifying loading UX * Update router * Fix i18n ID Co-authored-by: Constance --- .../components/api_logs/api_logs.test.tsx | 24 +++--- .../components/api_logs/api_logs.tsx | 73 ++++++++----------- .../components/api_logs_table.test.tsx | 10 +-- .../api_logs/components/api_logs_table.tsx | 20 ----- .../api_logs/components/empty_state.test.tsx | 27 +++++++ .../api_logs/components/empty_state.tsx | 45 ++++++++++++ .../components/api_logs/components/index.ts | 1 + .../components/engine/engine_router.tsx | 10 +-- 8 files changed, 124 insertions(+), 86 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx index c2a11ec06fa6a..5b082ce8d26ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx @@ -13,10 +13,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - -import { Loading } from '../../../shared/loading'; -import { rerender } from '../../../test_helpers'; +import { rerender, getPageTitle } from '../../../test_helpers'; import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention'; import { ApiLogsTable, NewApiEventsPrompt } from './components'; @@ -42,7 +39,7 @@ describe('ApiLogs', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs'); + expect(getPageTitle(wrapper)).toEqual('API Logs'); expect(wrapper.find(ApiLogsTable)).toHaveLength(1); expect(wrapper.find(NewApiEventsPrompt)).toHaveLength(1); @@ -50,11 +47,20 @@ describe('ApiLogs', () => { expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api'); }); - it('renders a loading screen', () => { - setMockValues({ ...values, dataLoading: true, apiLogs: [] }); - const wrapper = shallow(); + describe('loading state', () => { + it('renders a full-page loading state on initial page load (no logs exist yet)', () => { + setMockValues({ ...values, dataLoading: true, apiLogs: [] }); + const wrapper = shallow(); + + expect(wrapper.prop('isLoading')).toEqual(true); + }); + + it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => { + setMockValues({ ...values, dataLoading: true, apiLogs: [{}] }); + const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(false); + }); }); describe('effects', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx index b8179163c93f9..d3eef77db21f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx @@ -9,25 +9,14 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiPageHeader, - EuiTitle, - EuiPageContent, - EuiPageContentBody, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, -} from '@elastic/eui'; - -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention'; import { ApiLogFlyout } from './api_log'; -import { ApiLogsTable, NewApiEventsPrompt } from './components'; +import { ApiLogsTable, NewApiEventsPrompt, EmptyState } from './components'; import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants'; import { ApiLogsLogic } from './'; @@ -44,38 +33,36 @@ export const ApiLogs: React.FC = () => { pollForApiLogs(); }, []); - if (dataLoading && !apiLogs.length) return ; - return ( - <> - - - - + } + > - - - - - -

    {RECENT_API_EVENTS}

    - -
    - - - - - - - - - + + + + +

    {RECENT_API_EVENTS}

    +
    +
    + + + + + + + +
    + - - - - - + + +
    + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx index 2a00cc6eb42bb..82d3d4715cbc5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty } from '@elastic/eui'; import { DEFAULT_META } from '../../../../shared/constants'; import { mountWithIntl } from '../../../../test_helpers'; @@ -91,14 +91,6 @@ describe('ApiLogsTable', () => { expect(actions.openFlyout).toHaveBeenCalled(); }); - it('renders an empty prompt if no items are passed', () => { - setMockValues({ ...values, apiLogs: [] }); - const wrapper = mountWithIntl(); - const promptContent = wrapper.find(EuiEmptyPrompt).text(); - - expect(promptContent).toContain('Perform your first API call'); - }); - describe('hasPagination', () => { it('does not render with pagination by default', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx index bb1327ce2da30..1b5a8084f5b59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -15,7 +15,6 @@ import { EuiBadge, EuiHealth, EuiButtonEmpty, - EuiEmptyPrompt, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative } from '@kbn/i18n/react'; @@ -109,25 +108,6 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => { items={apiLogs} responsive loading={dataLoading} - noItemsMessage={ - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', { - defaultMessage: 'Perform your first API call', - })} - - } - body={ -

    - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', { - defaultMessage: "Check back after you've performed some API calls.", - })} -

    - } - /> - } {...paginationProps} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx new file mode 100644 index 0000000000000..3ad22ceac5840 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EmptyState } from './'; + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow() + .find(EuiEmptyPrompt) + .dive(); + + expect(wrapper.find('h2').text()).toEqual('Perform your first API call'); + expect(wrapper.find(EuiButton).prop('href')).toEqual( + expect.stringContaining('/api-reference.html') + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx new file mode 100644 index 0000000000000..3f6f44adefc71 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx @@ -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 React from 'react'; + +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DOCS_PREFIX } from '../../../routes'; + +export const EmptyState: React.FC = () => ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', { + defaultMessage: 'Perform your first API call', + })} + + } + body={ +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', { + defaultMessage: "Check back after you've performed some API calls.", + })} +

    + } + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.empty.buttonLabel', { + defaultMessage: 'View the API reference', + })} + + } + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts index c0edc51d06228..863216554a540 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts @@ -7,3 +7,4 @@ export { ApiLogsTable } from './api_logs_table'; export { NewApiEventsPrompt } from './new_api_events_prompt'; +export { EmptyState } from './empty_state'; 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 3e18c9e680de2..fc057858426d2 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 @@ -114,6 +114,11 @@ export const EngineRouter: React.FC = () => { )} + {canViewEngineApiLogs && ( + + + + )} {/* TODO: Remove layout once page template migration is over */} }> {canViewEngineSchema && ( @@ -141,11 +146,6 @@ export const EngineRouter: React.FC = () => { )} - {canViewEngineApiLogs && ( - - - - )} {canViewMetaEngineSourceEngines && ( From bd884a8828085e9632e0f0d30f705bfc2800c9c4 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 22 Jun 2021 22:15:46 +0100 Subject: [PATCH 078/118] chore(NA): moving @kbn/ui-framework into bazel (#102908) (#102992) # Conflicts: # x-pack/package.json --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-ui-framework/BUILD.bazel | 47 +++++++++++++++++++ x-pack/package.json | 4 -- yarn.lock | 2 +- 6 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 packages/kbn-ui-framework/BUILD.bazel diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index ebab9de66032f..4d8890e3fc99d 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -103,6 +103,7 @@ yarn kbn watch-bazel - @kbn/storybook - @kbn/telemetry-utils - @kbn/tinymath +- @kbn/ui-framework - @kbn/ui-shared-deps - @kbn/utility-types - @kbn/utils diff --git a/package.json b/package.json index 01b4a4f58de17..cd96fde1594b8 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath", - "@kbn/ui-framework": "link:packages/kbn-ui-framework", + "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:bazel-bin/packages/kbn-ui-shared-deps", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types", "@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 61034c562b447..bce279545b49c 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -47,6 +47,7 @@ filegroup( "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", + "//packages/kbn-ui-framework:build", "//packages/kbn-ui-shared-deps:build", "//packages/kbn-utility-types:build", "//packages/kbn-utils:build", diff --git a/packages/kbn-ui-framework/BUILD.bazel b/packages/kbn-ui-framework/BUILD.bazel new file mode 100644 index 0000000000000..f8cf5035bdc5f --- /dev/null +++ b/packages/kbn-ui-framework/BUILD.bazel @@ -0,0 +1,47 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-ui-framework" +PKG_REQUIRE_NAME = "@kbn/ui-framework" + +SOURCE_FILES = glob([ + "dist/**/*", +]) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +DEPS = [] + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [ + ":srcs", + ], + deps = DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/x-pack/package.json b/x-pack/package.json index 59b9b21e239dd..9f18b3b8479a8 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -28,9 +28,5 @@ "devDependencies": { "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test" - }, - "dependencies": { - "@kbn/interpreter": "link:../packages/kbn-interpreter", - "@kbn/ui-framework": "link:../packages/kbn-ui-framework" } } diff --git a/yarn.lock b/yarn.lock index de4b8fddce53e..c06b06fb14123 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2788,7 +2788,7 @@ version "0.0.0" uid "" -"@kbn/ui-framework@link:packages/kbn-ui-framework": +"@kbn/ui-framework@link:bazel-bin/packages/kbn-ui-framework": version "0.0.0" uid "" From af7e111ccb63f994f8f889c6ce17215da927fe41 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:17:48 -0400 Subject: [PATCH 079/118] [canvas] New Home Page (#102446) (#102993) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Clint Andrew Hall --- x-pack/plugins/canvas/i18n/components.ts | 221 ------- x-pack/plugins/canvas/i18n/errors.ts | 53 +- .../public/components/home/home.component.tsx | 67 +++ .../public/components/home/home.stories.tsx | 30 + .../canvas/public/components/home/home.tsx | 33 + .../public/components/home/hooks/index.ts | 15 + .../home/hooks/use_clone_workpad.ts | 60 ++ .../home/hooks/use_create_from_template.ts | 32 + .../home/hooks/use_create_workpad.ts | 46 ++ .../home/hooks/use_delete_workpad.ts | 63 ++ .../home/hooks/use_download_workpad.ts | 12 + .../home/hooks/use_find_templates.ts | 38 ++ .../components/home/hooks/use_find_workpad.ts | 57 ++ .../home/hooks/use_upload_workpad.ts | 100 ++++ .../index.js => home/index.ts} | 2 +- .../home/my_workpads/empty_prompt.stories.tsx | 19 + .../home/my_workpads/empty_prompt.tsx | 65 ++ .../components/home/my_workpads/index.ts | 10 + .../components/home/my_workpads/loading.tsx | 17 + .../my_workpads/my_workpads.component.tsx | 38 ++ .../home/my_workpads/my_workpads.stories.tsx | 56 ++ .../home/my_workpads/my_workpads.tsx | 42 ++ .../my_workpads/upload_dropzone.component.tsx | 30 + .../home/my_workpads/upload_dropzone.scss | 8 + .../home/my_workpads/upload_dropzone.tsx | 55 ++ .../my_workpads/workpad_import.component.tsx | 40 ++ .../home/my_workpads/workpad_import.tsx | 35 ++ .../my_workpads/workpad_table.component.tsx | 203 +++++++ .../my_workpads/workpad_table.stories.tsx | 83 +++ .../home/my_workpads/workpad_table.tsx | 38 ++ .../workpad_table_tools.component.tsx | 160 +++++ .../home/my_workpads/workpad_table_tools.tsx | 51 ++ .../home/workpad_create.component.tsx | 37 ++ .../public/components/home/workpad_create.tsx | 31 + .../home/workpad_templates/index.ts | 10 + .../workpad_templates.component.tsx | 157 +++++ .../workpad_templates.stories.tsx | 62 ++ .../workpad_templates/workpad_templates.tsx | 35 ++ .../home_app/home_app.component.tsx | 18 +- .../toolbar/__stories__/toolbar.stories.tsx | 2 - .../components/toolbar/toolbar.component.tsx | 36 +- .../components/workpad_loader/index.tsx | 173 ------ .../workpad_loader/upload_workpad.js | 52 -- .../workpad_loader/workpad_create.js | 31 - .../workpad_loader/workpad_dropzone/index.js | 31 - .../workpad_dropzone/workpad_dropzone.js | 31 - .../workpad_dropzone/workpad_dropzone.scss | 22 - .../workpad_loader/workpad_loader.js | 426 ------------- .../workpad_loader/workpad_loader.scss | 25 - .../workpad_loader/workpad_search.js | 44 -- .../workpad_manager/workpad_manager.js | 69 --- .../workpad_templates.stories.storyshot | 564 ------------------ .../examples/workpad_templates.stories.tsx | 45 -- .../components/workpad_templates/index.tsx | 86 --- .../workpad_templates/workpad_templates.tsx | 215 ------- .../canvas/public/lib/get_tags_filter.tsx | 39 -- .../plugins/canvas/public/services/index.ts | 2 +- .../canvas/public/services/stubs/platform.ts | 8 +- .../canvas/public/services/stubs/workpad.ts | 96 ++- .../plugins/canvas/public/services/workpad.ts | 21 +- x-pack/plugins/canvas/public/style/index.scss | 2 - .../canvas/storybook/decorators/index.ts | 3 +- .../storybook/decorators/redux_decorator.tsx | 2 +- .../decorators/services_decorator.tsx | 40 +- x-pack/plugins/canvas/storybook/index.ts | 5 + x-pack/plugins/canvas/storybook/main.ts | 5 + .../empty_prompt.stories.storyshot | 65 ++ .../canvas/storybook/storyshots.test.tsx | 7 +- .../translations/translations/ja-JP.json | 81 ++- .../translations/translations/zh-CN.json | 81 ++- x-pack/test/accessibility/apps/canvas.ts | 2 +- .../test/functional/apps/canvas/smoke_test.js | 2 +- .../functional/page_objects/canvas_page.ts | 2 +- 73 files changed, 2156 insertions(+), 2288 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/home/home.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/home.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/home.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/index.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts rename x-pack/plugins/canvas/public/components/{workpad_manager/index.js => home/index.ts} (83%) create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/index.ts create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_create.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/index.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/index.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx delete mode 100644 x-pack/plugins/canvas/public/lib/get_tags_filter.tsx create mode 100644 x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 7a23137e7ef60..6f011bb73e3b0 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -1166,12 +1166,6 @@ export const ComponentStrings = { description: 'This is referring to the dimensions of U.S. standard letter paper.', }), }, - WorkpadCreate: { - getWorkpadCreateButtonLabel: () => - i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', { - defaultMessage: 'Create workpad', - }), - }, WorkpadHeader: { getAddElementButtonLabel: () => i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', { @@ -1546,219 +1540,4 @@ export const ComponentStrings = { defaultMessage: 'Reset', }), }, - WorkpadLoader: { - getClonedWorkpadName: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.clonedWorkpadName', { - defaultMessage: 'Copy of {workpadName}', - values: { - workpadName, - }, - description: - 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' + - 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"', - }), - getCloneToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.cloneTooltip', { - defaultMessage: 'Clone workpad', - }), - getCreateWorkpadLoadingDescription: () => - i18n.translate('xpack.canvas.workpadLoader.createWorkpadLoadingDescription', { - defaultMessage: 'Creating workpad...', - description: - 'This message appears while the user is waiting for a new workpad to be created', - }), - getDeleteButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.deleteButtonAriaLabel', { - defaultMessage: 'Delete {numberOfWorkpads} workpads', - values: { - numberOfWorkpads, - }, - }), - getDeleteButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.deleteButtonLabel', { - defaultMessage: 'Delete ({numberOfWorkpads})', - values: { - numberOfWorkpads, - }, - }), - getDeleteModalConfirmButtonLabel: () => - i18n.translate('xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel', { - defaultMessage: 'Delete', - }), - getDeleteModalDescription: () => - i18n.translate('xpack.canvas.workpadLoader.deleteModalDescription', { - defaultMessage: `You can't recover deleted workpads.`, - }), - getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) => - i18n.translate('xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle', { - defaultMessage: 'Delete {numberOfWorkpads} workpads?', - values: { - numberOfWorkpads, - }, - }), - getDeleteSingleWorkpadModalTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle', { - defaultMessage: `Delete workpad '{workpadName}'?`, - values: { - workpadName, - }, - }), - getEmptyPromptGettingStartedDescription: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription', { - defaultMessage: - 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.', - values: { - JSON, - }, - }), - getEmptyPromptNewUserDescription: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptNewUserDescription', { - defaultMessage: 'New to {CANVAS}?', - values: { - CANVAS, - }, - }), - getEmptyPromptTitle: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptTitle', { - defaultMessage: 'Add your first workpad', - }), - getExportButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.exportButtonAriaLabel', { - defaultMessage: 'Export {numberOfWorkpads} workpads', - values: { - numberOfWorkpads, - }, - }), - getExportButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.exportButtonLabel', { - defaultMessage: 'Export ({numberOfWorkpads})', - values: { - numberOfWorkpads, - }, - }), - getExportToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.exportTooltip', { - defaultMessage: 'Export workpad', - }), - getFetchLoadingDescription: () => - i18n.translate('xpack.canvas.workpadLoader.fetchLoadingDescription', { - defaultMessage: 'Fetching workpads...', - description: - 'This message appears while the user is waiting for their list of workpads to load', - }), - getFilePickerPlaceholder: () => - i18n.translate('xpack.canvas.workpadLoader.filePickerPlaceholder', { - defaultMessage: 'Import workpad {JSON} file', - values: { - JSON, - }, - }), - getLoadWorkpadArialLabel: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.loadWorkpadArialLabel', { - defaultMessage: `Load workpad '{workpadName}'`, - values: { - workpadName, - }, - }), - getNoPermissionToCloneToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToCloneToolTip', { - defaultMessage: `You don't have permission to clone workpads`, - }), - getNoPermissionToCreateToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToCreateToolTip', { - defaultMessage: `You don't have permission to create workpads`, - }), - getNoPermissionToDeleteToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToDeleteToolTip', { - defaultMessage: `You don't have permission to delete workpads`, - }), - getNoPermissionToUploadToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToUploadToolTip', { - defaultMessage: `You don't have permission to upload workpads`, - }), - getSampleDataLinkLabel: () => - i18n.translate('xpack.canvas.workpadLoader.sampleDataLinkLabel', { - defaultMessage: 'Add your first workpad', - }), - getTableCreatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.createdColumnTitle', { - defaultMessage: 'Created', - description: 'This column in the table contains the date/time the workpad was created.', - }), - getTableNameColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.nameColumnTitle', { - defaultMessage: 'Workpad name', - }), - getTableUpdatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.updatedColumnTitle', { - defaultMessage: 'Updated', - description: - 'This column in the table contains the date/time the workpad was last updated.', - }), - getTableActionsColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', { - defaultMessage: 'Actions', - description: - 'This column in the table contains the actions that can be taken on a workpad.', - }), - }, - WorkpadManager: { - getModalTitle: () => - i18n.translate('xpack.canvas.workpadManager.modalTitle', { - defaultMessage: '{CANVAS} workpads', - values: { - CANVAS, - }, - }), - getMyWorkpadsTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', { - defaultMessage: 'My workpads', - }), - getWorkpadTemplatesTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', { - defaultMessage: 'Templates', - description: 'The label for the tab that displays a list of designed workpad templates.', - }), - }, - WorkpadSearch: { - getWorkpadSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadSearch.searchPlaceholder', { - defaultMessage: 'Find workpad', - }), - }, - WorkpadTemplates: { - getCloneTemplateLinkAriaLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel', { - defaultMessage: `Clone workpad template '{templateName}'`, - values: { - templateName, - }, - }), - getTableDescriptionColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', { - defaultMessage: 'Description', - }), - getTableNameColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', { - defaultMessage: 'Template name', - }), - getTableTagsColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', { - defaultMessage: 'Tags', - description: - 'This column contains relevant tags that indicate what type of template ' + - 'is displayed. For example: "report", "presentation", etc.', - }), - getTemplateSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadTemplate.searchPlaceholder', { - defaultMessage: 'Find template', - }), - getCreatingTemplateLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplate.creatingTemplateLabel', { - defaultMessage: `Creating from template '{templateName}'`, - values: { - templateName, - }, - }), - }, }; diff --git a/x-pack/plugins/canvas/i18n/errors.ts b/x-pack/plugins/canvas/i18n/errors.ts index 0928045119234..a55762dce2d20 100644 --- a/x-pack/plugins/canvas/i18n/errors.ts +++ b/x-pack/plugins/canvas/i18n/errors.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { CANVAS, JSON } from './constants'; export const ErrorStrings = { actionsElements: { @@ -93,54 +92,10 @@ export const ErrorStrings = { }, }), }, - WorkpadFileUpload: { - getAcceptJSONOnlyErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage', { - defaultMessage: 'Only {JSON} files are accepted', - values: { - JSON, - }, - }), - getFileUploadFailureWithFileNameErrorMessage: (fileName: string) => - i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', { - defaultMessage: `Couldn't upload '{fileName}'`, - values: { - fileName, - }, - }), - getFileUploadFailureWithoutFileNameErrorMessage: () => - i18n.translate( - 'xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage', - { - defaultMessage: `Couldn't upload file`, - } - ), - getMissingPropertiesErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', { - defaultMessage: - 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.', - values: { - CANVAS, - JSON, - }, - }), - }, - WorkpadLoader: { - getCloneFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', { - defaultMessage: `Couldn't clone workpad`, - }), - getDeleteFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.deleteFailureErrorMessage', { - defaultMessage: `Couldn't delete all workpads`, - }), - getFindFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.findFailureErrorMessage', { - defaultMessage: `Couldn't find workpad`, - }), - getUploadFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', { - defaultMessage: `Couldn't upload workpad`, + WorkpadDropzone: { + getTooManyFilesErrorMessage: () => + i18n.translate('xpack.canvas.error.workpadDropzone.tooManyFilesErrorMessage', { + defaultMessage: 'One one file can be uploaded at a time', }), }, workpadRoutes: { diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx new file mode 100644 index 0000000000000..96a773186da2b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx @@ -0,0 +1,67 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public'; +import { withSuspense } from '../../../../../../src/plugins/presentation_util/public'; + +import { WorkpadCreate } from './workpad_create'; +import { LazyWorkpadTemplates } from './workpad_templates'; +import { LazyMyWorkpads } from './my_workpads'; + +export type HomePageTab = 'workpads' | 'templates'; + +export interface Props { + activeTab?: HomePageTab; +} + +const WorkpadTemplates = withSuspense(LazyWorkpadTemplates); +const MyWorkpads = withSuspense(LazyMyWorkpads); + +export const Home = ({ activeTab = 'workpads' }: Props) => { + const [tab, setTab] = useState(activeTab); + + return ( + ], + bottomBorder: true, + tabs: [ + { + label: strings.getMyWorkpadsTabLabel(), + id: 'myWorkpads', + isSelected: tab === 'workpads', + onClick: () => setTab('workpads'), + }, + { + label: strings.getWorkpadTemplatesTabLabel(), + id: 'workpadTemplates', + 'data-test-subj': 'workpadTemplates', + isSelected: tab === 'templates', + onClick: () => setTab('templates'), + }, + ], + }} + > + {tab === 'workpads' ? : } + + ); +}; + +const strings = { + getMyWorkpadsTabLabel: () => + i18n.translate('xpack.canvas.home.myWorkpadsTabLabel', { + defaultMessage: 'My workpads', + }), + getWorkpadTemplatesTabLabel: () => + i18n.translate('xpack.canvas.home.workpadTemplatesTabLabel', { + defaultMessage: 'Templates', + description: 'The label for the tab that displays a list of designed workpad templates.', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/home.stories.tsx b/x-pack/plugins/canvas/public/components/home/home.stories.tsx new file mode 100644 index 0000000000000..186b916afa003 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/home.stories.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + reduxDecorator, + getAddonPanelParameters, + servicesContextDecorator, + getDisableStoryshotsParameter, +} from '../../../storybook'; + +import { Home } from './home.component'; + +export default { + title: 'Home/Home Page', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, +}; + +export const NoContent = () => ; +export const HasContent = () => ; + +NoContent.decorators = [servicesContextDecorator()]; +HasContent.decorators = [servicesContextDecorator({ findWorkpads: 5, findTemplates: true })]; diff --git a/x-pack/plugins/canvas/public/components/home/home.tsx b/x-pack/plugins/canvas/public/components/home/home.tsx new file mode 100644 index 0000000000000..6b356ada8681e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/home.tsx @@ -0,0 +1,33 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; +import { resetWorkpad } from '../../state/actions/workpad'; +import { Home as Component } from './home.component'; +import { usePlatformService } from '../../services'; + +export const Home = () => { + const { setBreadcrumbs } = usePlatformService(); + const [isMounted, setIsMounted] = useState(false); + const dispatch = useDispatch(); + + useEffect(() => { + if (!isMounted) { + dispatch(resetWorkpad()); + setIsMounted(true); + } + }, [dispatch, isMounted, setIsMounted]); + + useEffect(() => { + setBreadcrumbs([getBaseBreadcrumb()]); + }, [setBreadcrumbs]); + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/index.ts b/x-pack/plugins/canvas/public/components/home/hooks/index.ts new file mode 100644 index 0000000000000..91e52948a7ba6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { useCloneWorkpad } from './use_clone_workpad'; +export { useCreateWorkpad } from './use_create_workpad'; +export { useDeleteWorkpads } from './use_delete_workpad'; +export { useDownloadWorkpad } from './use_download_workpad'; +export { useFindTemplates, useFindTemplatesOnMount } from './use_find_templates'; +export { useFindWorkpads, useFindWorkpadsOnMount } from './use_find_workpad'; +export { useImportWorkpad } from './use_upload_workpad'; +export { useCreateFromTemplate } from './use_create_from_template'; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts new file mode 100644 index 0000000000000..001a711a58a72 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +import { useNotifyService, useWorkpadService } from '../../../services'; +import { getId } from '../../../lib/get_id'; + +export const useCloneWorkpad = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (workpadId: string) => { + try { + let workpad = await workpadService.get(workpadId); + + workpad = { + ...workpad, + name: strings.getClonedWorkpadName(workpad.name), + id: getId('workpad'), + }; + + await workpadService.create(workpad); + + history.push(`/workpad/${workpad.id}/page/1`); + } catch (err) { + notifyService.error(err, { title: errors.getCloneFailureErrorMessage() }); + } + }, + [notifyService, workpadService, history] + ); +}; + +const strings = { + getClonedWorkpadName: (workpadName: string) => + i18n.translate('xpack.canvas.useCloneWorkpad.clonedWorkpadName', { + defaultMessage: 'Copy of {workpadName}', + values: { + workpadName, + }, + description: + 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' + + 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"', + }), +}; + +const errors = { + getCloneFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage', { + defaultMessage: `Couldn't clone workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts new file mode 100644 index 0000000000000..968f9398ba857 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts @@ -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 { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { CanvasTemplate } from '../../../../types'; +import { useNotifyService, useWorkpadService } from '../../../services'; + +export const useCreateFromTemplate = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (template: CanvasTemplate) => { + try { + const result = await workpadService.createFromTemplate(template.id); + history.push(`/workpad/${result.id}/page/1`); + } catch (e) { + notifyService.error(e, { + title: `Couldn't create workpad from template`, + }); + } + }, + [workpadService, notifyService, history] + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts new file mode 100644 index 0000000000000..eb87f4720deec --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts @@ -0,0 +1,46 @@ +/* + * 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 { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +// @ts-expect-error +import { getDefaultWorkpad } from '../../../state/defaults'; +import { useNotifyService, useWorkpadService } from '../../../services'; + +import type { CanvasWorkpad } from '../../../../types'; + +export const useCreateWorkpad = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (_workpad?: CanvasWorkpad | null) => { + const workpad = _workpad || (getDefaultWorkpad() as CanvasWorkpad); + + try { + await workpadService.create(workpad); + history.push(`/workpad/${workpad.id}/page/1`); + } catch (err) { + notifyService.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); + } + return; + }, + [notifyService, history, workpadService] + ); +}; + +const errors = { + getUploadFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', { + defaultMessage: `Couldn't upload workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts new file mode 100644 index 0000000000000..722ddae7411c9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.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 { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { useNotifyService, useWorkpadService } from '../../../services'; + +export const useDeleteWorkpads = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + + return useCallback( + async (workpadIds: string[]) => { + const removedWorkpads = workpadIds.map(async (id) => { + try { + await workpadService.remove(id); + return { id, err: null }; + } catch (err) { + return { id, err }; + } + }); + + return Promise.all(removedWorkpads).then((results) => { + const [passes, errored] = results.reduce<[string[], string[]]>( + ([passesArr, errorsArr], result) => { + if (result.err) { + errorsArr.push(result.id); + } else { + passesArr.push(result.id); + } + + return [passesArr, errorsArr]; + }, + [[], []] + ); + + const removedIds = workpadIds.filter((id) => passes.includes(id)); + + if (errored.length > 0) { + notifyService.error(errors.getDeleteFailureErrorMessage()); + } + + return { + removedIds, + errored, + }; + }); + }, + [workpadService, notifyService] + ); +}; + +const errors = { + getDeleteFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage', { + defaultMessage: `Couldn't delete all workpads`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts new file mode 100644 index 0000000000000..b875e08c2a230 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { downloadWorkpad as downloadWorkpadFn } from '../../../lib/download_workpad'; + +export const useDownloadWorkpad = () => + useCallback((workpadId: string) => downloadWorkpadFn(workpadId), []); diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts new file mode 100644 index 0000000000000..13ee289fe9867 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import useMount from 'react-use/lib/useMount'; + +import { useWorkpadService } from '../../../services'; +import { TemplateFindResponse } from '../../../services/workpad'; + +const emptyResponse = { templates: [] }; + +export const useFindTemplates = () => { + const workpadService = useWorkpadService(); + return useCallback(async () => await workpadService.findTemplates(), [workpadService]); +}; + +export const useFindTemplatesOnMount = (): [boolean, TemplateFindResponse] => { + const [isMounted, setIsMounted] = useState(false); + const findTemplates = useFindTemplates(); + const [templateResponse, setTemplateResponse] = useState(emptyResponse); + + const fetchTemplates = useCallback(async () => { + const foundTemplates = await findTemplates(); + setTemplateResponse(foundTemplates || emptyResponse); + setIsMounted(true); + }, [findTemplates]); + + useMount(() => { + fetchTemplates(); + return () => setIsMounted(false); + }); + + return [isMounted, templateResponse]; +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts new file mode 100644 index 0000000000000..3f8b0e6f630f5 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.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 { useState, useCallback } from 'react'; +import useMount from 'react-use/lib/useMount'; +import { i18n } from '@kbn/i18n'; + +import { WorkpadFindResponse } from '../../../services/workpad'; + +import { useNotifyService, useWorkpadService } from '../../../services'; +const emptyResponse = { total: 0, workpads: [] }; + +export const useFindWorkpads = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + + return useCallback( + async (text = '') => { + try { + return await workpadService.find(text); + } catch (err) { + notifyService.error(err, { title: errors.getFindFailureErrorMessage() }); + } + }, + [notifyService, workpadService] + ); +}; + +export const useFindWorkpadsOnMount = (): [boolean, WorkpadFindResponse] => { + const [isMounted, setIsMounted] = useState(false); + const findWorkpads = useFindWorkpads(); + const [workpadResponse, setWorkpadResponse] = useState(emptyResponse); + + const fetchWorkpads = useCallback(async () => { + const foundWorkpads = await findWorkpads(); + setWorkpadResponse(foundWorkpads || emptyResponse); + setIsMounted(true); + }, [findWorkpads]); + + useMount(() => { + fetchWorkpads(); + return () => setIsMounted(false); + }); + + return [isMounted, workpadResponse]; +}; + +const errors = { + getFindFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useFindWorkpads.findFailureErrorMessage', { + defaultMessage: `Couldn't find workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts new file mode 100644 index 0000000000000..7934a469bb7a2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts @@ -0,0 +1,100 @@ +/* + * 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 { useCallback } from 'react'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { CANVAS, JSON as JSONString } from '../../../../i18n/constants'; +import { useNotifyService } from '../../../services'; +import { getId } from '../../../lib/get_id'; +import type { CanvasWorkpad } from '../../../../types'; + +export const useImportWorkpad = () => { + const notifyService = useNotifyService(); + + return useCallback( + (file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => { + if (!file) { + onComplete(); + return; + } + + if (get(file, 'type') !== 'application/json') { + notifyService.warning(errors.getAcceptJSONOnlyErrorMessage(), { + title: file.name + ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) + : errors.getFileUploadFailureWithoutFileNameErrorMessage(), + }); + onComplete(); + } + + // TODO: Clean up this file, this loading stuff can, and should be, abstracted + const reader = new FileReader(); + + // handle reading the uploaded file + reader.onload = () => { + try { + const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + workpad.id = getId('workpad'); + + // sanity check for workpad object + if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) { + onComplete(); + throw new Error(errors.getMissingPropertiesErrorMessage()); + } + + onComplete(workpad); + } catch (e) { + notifyService.error(e, { + title: file.name + ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) + : errors.getFileUploadFailureWithoutFileNameErrorMessage(), + }); + onComplete(); + } + }; + + // read the uploaded file + reader.readAsText(file); + }, + [notifyService] + ); +}; + +const errors = { + getFileUploadFailureWithoutFileNameErrorMessage: () => + i18n.translate( + 'xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage', + { + defaultMessage: `Couldn't upload file`, + } + ), + getFileUploadFailureWithFileNameErrorMessage: (fileName: string) => + i18n.translate('xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage', { + defaultMessage: `Couldn't upload '{fileName}'`, + values: { + fileName, + }, + }), + getMissingPropertiesErrorMessage: () => + i18n.translate('xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage', { + defaultMessage: + 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.', + values: { + CANVAS, + JSON: JSONString, + }, + }), + getAcceptJSONOnlyErrorMessage: () => + i18n.translate('xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage', { + defaultMessage: 'Only {JSON} files are accepted', + values: { + JSON: JSONString, + }, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/index.js b/x-pack/plugins/canvas/public/components/home/index.ts similarity index 83% rename from x-pack/plugins/canvas/public/components/workpad_manager/index.js rename to x-pack/plugins/canvas/public/components/home/index.ts index e1f5855e762af..aeb62c3a8de78 100644 --- a/x-pack/plugins/canvas/public/components/workpad_manager/index.js +++ b/x-pack/plugins/canvas/public/components/home/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { WorkpadManager } from './workpad_manager'; +export { Home } from './home'; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx new file mode 100644 index 0000000000000..aef1b0625b585 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { HomeEmptyPrompt } from './empty_prompt'; +import { getDisableStoryshotsParameter } from '../../../../storybook'; + +export default { + title: 'Home/Empty Prompt', + argTypes: {}, + parameters: { ...getDisableStoryshotsParameter() }, +}; + +export const EmptyPrompt = () => ; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx new file mode 100644 index 0000000000000..797f50ac112d0 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { CANVAS, JSON } from '../../../../i18n/constants'; + +export const HomeEmptyPrompt = () => ( + + + + {strings.getEmptyPromptTitle()}} + titleSize="m" + body={ + +

    {strings.getEmptyPromptGettingStartedDescription()}

    +

    + {strings.getEmptyPromptNewUserDescription()}{' '} + + {strings.getSampleDataLinkLabel()} + + . +

    +
    + } + /> +
    +
    +
    +); + +const strings = { + getEmptyPromptGettingStartedDescription: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription', { + defaultMessage: + 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.', + values: { + JSON, + }, + }), + getEmptyPromptNewUserDescription: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription', { + defaultMessage: 'New to {CANVAS}?', + values: { + CANVAS, + }, + }), + getEmptyPromptTitle: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptTitle', { + defaultMessage: 'Add your first workpad', + }), + getSampleDataLinkLabel: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel', { + defaultMessage: 'Add your first workpad', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts b/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts new file mode 100644 index 0000000000000..79b1519df90fe --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +export const LazyMyWorkpads = React.lazy(() => import('./my_workpads')); diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx new file mode 100644 index 0000000000000..28edfea7c36ca --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx @@ -0,0 +1,17 @@ +/* + * 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 { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export const Loading = () => ( + + + + + +); diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx new file mode 100644 index 0000000000000..d9e3f0e4e2c99 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FoundWorkpad } from '../../../services/workpad'; +import { UploadDropzone } from './upload_dropzone'; +import { HomeEmptyPrompt } from './empty_prompt'; +import { WorkpadTable } from './workpad_table'; + +export interface Props { + workpads: FoundWorkpad[]; +} + +export const MyWorkpads = ({ workpads }: Props) => { + if (workpads.length === 0) { + return ( + + + + + + + + ); + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx new file mode 100644 index 0000000000000..0d5d6ca16f614 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiPanel } from '@elastic/eui'; + +import { + reduxDecorator, + getAddonPanelParameters, + servicesContextDecorator, + getDisableStoryshotsParameter, +} from '../../../../storybook'; +import { getSomeWorkpads } from '../../../services/stubs/workpad'; + +import { MyWorkpads, WorkpadsContext } from './my_workpads'; +import { MyWorkpads as MyWorkpadsComponent } from './my_workpads.component'; + +export default { + title: 'Home/My Workpads', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, +}; + +export const NoWorkpads = () => { + return ; +}; + +export const HasWorkpads = () => { + return ( + + + + ); +}; + +NoWorkpads.decorators = [servicesContextDecorator()]; +HasWorkpads.decorators = [servicesContextDecorator({ findWorkpads: 5 })]; + +export const Component = ({ workpadCount }: { workpadCount: number }) => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount)); + + return ( + + + + + + ); +}; + +Component.args = { workpadCount: 5 }; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx new file mode 100644 index 0000000000000..4242e2e9d130f --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, createContext, Dispatch, SetStateAction } from 'react'; +import { useFindWorkpadsOnMount } from './../hooks'; +import { FoundWorkpad } from '../../../services/workpad'; +import { Loading } from './loading'; +import { MyWorkpads as Component } from './my_workpads.component'; + +interface Context { + workpads: FoundWorkpad[]; + setWorkpads: Dispatch>; +} + +export const WorkpadsContext = createContext(null); + +export const MyWorkpads = () => { + const [isMounted, workpadResponse] = useFindWorkpadsOnMount(); + const [workpads, setWorkpads] = useState(workpadResponse.workpads); + + useEffect(() => { + setWorkpads(workpadResponse.workpads); + }, [workpadResponse]); + + if (!isMounted) { + return ; + } + + return ( + + + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default MyWorkpads; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx new file mode 100644 index 0000000000000..603f4679a9e95 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +// @ts-expect-error untyped library +import Dropzone from 'react-dropzone'; + +import './upload_dropzone.scss'; + +export interface Props { + disabled?: boolean; + onDrop?: (files: FileList) => void; +} + +export const UploadDropzone: FC = ({ onDrop = () => {}, disabled, children }) => { + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss new file mode 100644 index 0000000000000..e4ee284c72dee --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss @@ -0,0 +1,8 @@ +.canvasWorkpad__dropzone { + border: 2px dashed transparent; +} + +.canvasWorkpad__dropzone--active { + background-color: $euiColorLightestShade; + border-color: $euiColorLightShade; +} diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx new file mode 100644 index 0000000000000..8ee0ae108392e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState } from 'react'; +// @ts-expect-error untyped library +import Dropzone from 'react-dropzone'; + +import { useNotifyService } from '../../../services'; +import { ErrorStrings } from '../../../../i18n'; +import { useImportWorkpad, useCreateWorkpad } from '../hooks'; +import { CanvasWorkpad } from '../../../../types'; + +import { UploadDropzone as Component } from './upload_dropzone.component'; + +const { WorkpadDropzone: errors } = ErrorStrings; + +export const UploadDropzone: FC = ({ children }) => { + const notify = useNotifyService(); + const uploadWorkpad = useImportWorkpad(); + const createWorkpad = useCreateWorkpad(); + const [isDisabled, setIsDisabled] = useState(false); + + const onComplete = async (workpad?: CanvasWorkpad) => { + if (!workpad) { + setIsDisabled(false); + return; + } + + await createWorkpad(workpad); + }; + + const onDrop = (files: FileList) => { + if (!files) { + return; + } + + if (files.length > 1) { + notify.warning(errors.getTooManyFilesErrorMessage()); + return; + } + + setIsDisabled(true); + uploadWorkpad(files[0], onComplete); + }; + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx new file mode 100644 index 0000000000000..28e2aa0449d46 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFilePicker, EuiFilePickerProps } from '@elastic/eui'; + +import { JSON } from '../../../../i18n/constants'; +export interface Props { + canUserWrite: boolean; + onImportWorkpad?: EuiFilePickerProps['onChange']; + uniqueKey?: string | number; +} + +export const WorkpadImport = ({ uniqueKey, canUserWrite, onImportWorkpad = () => {} }: Props) => ( + +); + +const strings = { + getFilePickerPlaceholder: () => + i18n.translate('xpack.canvas.workpadImport.filePickerPlaceholder', { + defaultMessage: 'Import workpad {JSON} file', + values: { + JSON, + }, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx new file mode 100644 index 0000000000000..0f1ba621e14d7 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx @@ -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 React, { useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; +import type { State } from '../../../../types'; + +import { useImportWorkpad } from '../hooks'; +import { WorkpadImport as Component, Props as ComponentProps } from './workpad_import.component'; + +type Props = Omit; + +export const WorkpadImport = (props: Props) => { + const importWorkpad = useImportWorkpad(); + const [uniqueKey, setUniqueKey] = useState(Date.now()); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + const onImportWorkpad: ComponentProps['onImportWorkpad'] = (files) => { + if (files) { + importWorkpad(files[0]); + } + setUniqueKey(Date.now()); + }; + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx new file mode 100644 index 0000000000000..5301a88844369 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx @@ -0,0 +1,203 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + EuiInMemoryTable, + EuiInMemoryTableProps, + EuiTableActionsColumnType, + EuiBasicTableColumn, + EuiToolTip, + EuiButtonIcon, + EuiTableSelectionType, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import moment from 'moment'; + +import { RoutingLink } from '../../routing'; +import { FoundWorkpad } from '../../../services/workpad'; +import { WorkpadTableTools } from './workpad_table_tools'; +import { WorkpadImport } from './workpad_import'; + +export interface Props { + workpads: FoundWorkpad[]; + canUserWrite: boolean; + dateFormat: string; + onExportWorkpad: (ids: string) => void; + onCloneWorkpad: (id: string) => void; +} + +const getDisplayName = (name: string, workpadId: string, loadedWorkpadId?: string) => { + const workpadName = name.length ? {name} : {workpadId}; + return workpadId === loadedWorkpadId ? {workpadName} : workpadName; +}; + +export const WorkpadTable = ({ + workpads, + canUserWrite, + dateFormat, + onExportWorkpad: onExport, + onCloneWorkpad, +}: Props) => { + const [selectedIds, setSelectedIds] = useState([]); + const formatDate = (date: string) => date && moment(date).format(dateFormat); + + const selection: EuiTableSelectionType = { + onSelectionChange: (selectedWorkpads) => { + setSelectedIds(selectedWorkpads.map((workpad) => workpad.id).filter((id) => !!id)); + }, + }; + + const actions: EuiTableActionsColumnType['actions'] = [ + { + render: (workpad: FoundWorkpad) => ( + + + + onExport(workpad.id)} + aria-label={strings.getExportToolTip()} + /> + + + + + onCloneWorkpad(workpad.id)} + aria-label={strings.getCloneToolTip()} + disabled={!canUserWrite} + /> + + + + ), + }, + ]; + + const search: EuiInMemoryTableProps['search'] = { + toolsLeft: + selectedIds.length > 0 ? : undefined, + toolsRight: , + box: { + schema: true, + incremental: true, + placeholder: strings.getWorkpadSearchPlaceholder(), + 'data-test-subj': 'tableListSearchBox', + }, + }; + + const columns: Array> = [ + { + field: 'name', + name: strings.getTableNameColumnTitle(), + sortable: true, + dataType: 'string', + render: (name, workpad) => ( + + {getDisplayName(name, workpad.id)} + + ), + }, + { + field: '@created', + name: strings.getTableCreatedColumnTitle(), + sortable: true, + dataType: 'date', + width: '20%', + render: (date: string) => formatDate(date), + }, + { + field: '@timestamp', + name: strings.getTableUpdatedColumnTitle(), + sortable: true, + dataType: 'date', + width: '20%', + render: (date: string) => formatDate(date), + }, + { name: strings.getTableActionsColumnTitle(), actions, width: '100px' }, + ]; + + return ( + + ); +}; + +const strings = { + getCloneToolTip: () => + i18n.translate('xpack.canvas.workpadTable.cloneTooltip', { + defaultMessage: 'Clone workpad', + }), + getExportToolTip: () => + i18n.translate('xpack.canvas.workpadTable.exportTooltip', { + defaultMessage: 'Export workpad', + }), + getLoadWorkpadArialLabel: (workpadName: string) => + i18n.translate('xpack.canvas.workpadTable.loadWorkpadArialLabel', { + defaultMessage: `Load workpad '{workpadName}'`, + values: { + workpadName, + }, + }), + getNoPermissionToCloneToolTip: () => + i18n.translate('xpack.canvas.workpadTable.noPermissionToCloneToolTip', { + defaultMessage: `You don't have permission to clone workpads`, + }), + getNoWorkpadsFoundMessage: () => + i18n.translate('xpack.canvas.workpadTable.noWorkpadsFoundMessage', { + defaultMessage: 'No workpads matched your search.', + }), + getWorkpadSearchPlaceholder: () => + i18n.translate('xpack.canvas.workpadTable.searchPlaceholder', { + defaultMessage: 'Find workpad', + }), + getTableCreatedColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.createdColumnTitle', { + defaultMessage: 'Created', + description: 'This column in the table contains the date/time the workpad was created.', + }), + getTableNameColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.nameColumnTitle', { + defaultMessage: 'Workpad name', + }), + getTableUpdatedColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.updatedColumnTitle', { + defaultMessage: 'Updated', + description: 'This column in the table contains the date/time the workpad was last updated.', + }), + getTableActionsColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.actionsColumnTitle', { + defaultMessage: 'Actions', + description: 'This column in the table contains the actions that can be taken on a workpad.', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx new file mode 100644 index 0000000000000..501a0a76a8589 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx @@ -0,0 +1,83 @@ +/* + * 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, useEffect } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import { action } from '@storybook/addon-actions'; +import { + reduxDecorator, + getAddonPanelParameters, + getDisableStoryshotsParameter, +} from '../../../../storybook'; +import { getSomeWorkpads } from '../../../services/stubs/workpad'; + +import { WorkpadTable } from './workpad_table'; +import { WorkpadTable as WorkpadTableComponent } from './workpad_table.component'; +import { WorkpadsContext } from './my_workpads'; + +export default { + title: 'Home/Workpad Table', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, +}; + +export const NoWorkpads = () => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(0)); + + return ( + + + + + + ); +}; + +export const HasWorkpads = () => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(5)); + + return ( + + + + + + ); +}; + +export const Component = ({ + workpadCount, + canUserWrite, + dateFormat, +}: { + workpadCount: number; + canUserWrite: boolean; + dateFormat: string; +}) => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount)); + + useEffect(() => { + setWorkpads(getSomeWorkpads(workpadCount)); + }, [workpadCount]); + + return ( + + + + + + ); +}; + +Component.args = { workpadCount: 5, canUserWrite: true, dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS' }; +Component.argTypes = {}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx new file mode 100644 index 0000000000000..e5d83039a87eb --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; +import type { State } from '../../../../types'; +import { usePlatformService } from '../../../services'; +import { useCloneWorkpad, useDownloadWorkpad } from '../hooks'; + +import { WorkpadTable as Component } from './workpad_table.component'; +import { WorkpadsContext } from './my_workpads'; + +export const WorkpadTable = () => { + const platformService = usePlatformService(); + const onCloneWorkpad = useCloneWorkpad(); + const onExportWorkpad = useDownloadWorkpad(); + const context = useContext(WorkpadsContext); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + if (!context) { + return null; + } + + const { workpads } = context; + + const dateFormat = platformService.getUISetting('dateFormat'); + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx new file mode 100644 index 0000000000000..ae6ff9c3cc910 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx @@ -0,0 +1,160 @@ +/* + * 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, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiToolTip, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; + +import { ConfirmModal } from '../../confirm_modal'; +import { FoundWorkpad } from '../../../services/workpad'; + +export interface Props { + workpads: FoundWorkpad[]; + canUserWrite: boolean; + selectedWorkpadIds: string[]; + onDeleteWorkpads: (ids: string[]) => void; + onExportWorkpads: (ids: string[]) => void; +} + +export const WorkpadTableTools = ({ + workpads, + canUserWrite, + selectedWorkpadIds, + onDeleteWorkpads, + onExportWorkpads, +}: Props) => { + const [isDeletePending, setIsDeletePending] = useState(false); + + const openRemoveConfirm = () => setIsDeletePending(true); + const closeRemoveConfirm = () => setIsDeletePending(false); + + let deleteButton = ( + + {strings.getDeleteButtonLabel(selectedWorkpadIds.length)} + + ); + + const downloadButton = ( + onExportWorkpads(selectedWorkpadIds)} + iconType="exportAction" + aria-label={strings.getExportButtonAriaLabel(selectedWorkpadIds.length)} + > + {strings.getExportButtonLabel(selectedWorkpadIds.length)} + + ); + + if (!canUserWrite) { + deleteButton = ( + {deleteButton} + ); + } + + const modalTitle = + selectedWorkpadIds.length === 1 + ? strings.getDeleteSingleWorkpadModalTitle( + workpads.find((workpad) => workpad.id === selectedWorkpadIds[0])?.name || '' + ) + : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpadIds.length + ''); + + const confirmModal = ( + { + onDeleteWorkpads(selectedWorkpadIds); + closeRemoveConfirm(); + }} + onCancel={closeRemoveConfirm} + /> + ); + + return ( + + + {downloadButton} + {deleteButton} + + {confirmModal} + + ); +}; + +const strings = { + getDeleteButtonAriaLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.deleteButtonAriaLabel', { + defaultMessage: 'Delete {numberOfWorkpads} workpads', + values: { + numberOfWorkpads, + }, + }), + getDeleteButtonLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.deleteButtonLabel', { + defaultMessage: 'Delete ({numberOfWorkpads})', + values: { + numberOfWorkpads, + }, + }), + getDeleteModalConfirmButtonLabel: () => + i18n.translate('xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel', { + defaultMessage: 'Delete', + }), + getDeleteModalDescription: () => + i18n.translate('xpack.canvas.workpadTableTools.deleteModalDescription', { + defaultMessage: `You can't recover deleted workpads.`, + }), + getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) => + i18n.translate('xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle', { + defaultMessage: 'Delete {numberOfWorkpads} workpads?', + values: { + numberOfWorkpads, + }, + }), + getDeleteSingleWorkpadModalTitle: (workpadName: string) => + i18n.translate('xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle', { + defaultMessage: `Delete workpad '{workpadName}'?`, + values: { + workpadName, + }, + }), + getExportButtonAriaLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.exportButtonAriaLabel', { + defaultMessage: 'Export {numberOfWorkpads} workpads', + values: { + numberOfWorkpads, + }, + }), + getExportButtonLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.exportButtonLabel', { + defaultMessage: 'Export ({numberOfWorkpads})', + values: { + numberOfWorkpads, + }, + }), + getNoPermissionToCreateToolTip: () => + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToCreateToolTip', { + defaultMessage: `You don't have permission to create workpads`, + }), + getNoPermissionToDeleteToolTip: () => + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip', { + defaultMessage: `You don't have permission to delete workpads`, + }), + getNoPermissionToUploadToolTip: () => + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToUploadToolTip', { + defaultMessage: `You don't have permission to upload workpads`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx new file mode 100644 index 0000000000000..62d84adfc2649 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx @@ -0,0 +1,51 @@ +/* + * 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, { useContext } from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; +import type { State } from '../../../../types'; +import { useDeleteWorkpads, useDownloadWorkpad } from '../hooks'; + +import { + WorkpadTableTools as Component, + Props as ComponentProps, +} from './workpad_table_tools.component'; +import { WorkpadsContext } from './my_workpads'; + +export type Props = Pick; + +export const WorkpadTableTools = ({ selectedWorkpadIds }: Props) => { + const deleteWorkpads = useDeleteWorkpads(); + const downloadWorkpad = useDownloadWorkpad(); + const context = useContext(WorkpadsContext); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + if (context === null || selectedWorkpadIds.length <= 0) { + return null; + } + + const { workpads, setWorkpads } = context; + + const onExport = () => selectedWorkpadIds.map((id) => downloadWorkpad(id)); + const onDelete = async () => { + const { removedIds } = await deleteWorkpads(selectedWorkpadIds); + setWorkpads(workpads.filter((workpad) => !removedIds.includes(workpad.id))); + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx new file mode 100644 index 0000000000000..18bdb97683194 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx @@ -0,0 +1,37 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiButton } from '@elastic/eui'; +import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button'; + +export interface Props + extends Omit { + canUserWrite: boolean; +} + +export const WorkpadCreate = ({ canUserWrite, disabled, ...rest }: Props) => { + return ( + + {strings.getWorkpadCreateButtonLabel()} + + ); +}; + +const strings = { + getWorkpadCreateButtonLabel: () => + i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', { + defaultMessage: 'Create workpad', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.tsx new file mode 100644 index 0000000000000..adb73a6bb8896 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_create.tsx @@ -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 React from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app'; +import type { State } from '../../../types'; + +import { useCreateWorkpad } from './hooks'; +import { WorkpadCreate as Component, Props as ComponentProps } from './workpad_create.component'; + +type Props = Omit; + +export const WorkpadCreate = (props: Props) => { + const createWorkpad = useCreateWorkpad(); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + const onClick: ComponentProps['onClick'] = async () => { + await createWorkpad(); + }; + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts new file mode 100644 index 0000000000000..4c45dbff38377 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +export const LazyWorkpadTemplates = React.lazy(() => import('./workpad_templates')); diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx new file mode 100644 index 0000000000000..d974c70b05cf2 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx @@ -0,0 +1,157 @@ +/* + * 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 { uniq } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiInMemoryTable, + EuiBasicTableColumn, + EuiButtonEmpty, + EuiSearchBarProps, + SearchFilterConfig, +} from '@elastic/eui'; + +import { CanvasTemplate } from '../../../../types'; +import { tagsRegistry } from '../../../lib/tags_registry'; +import { TagList } from '../../tag_list'; + +export interface Props { + templates: CanvasTemplate[]; + onCreateWorkpad: (template: CanvasTemplate) => void; +} + +export const WorkpadTemplates = ({ templates, onCreateWorkpad }: Props) => { + const columns: Array> = [ + { + field: 'name', + name: strings.getTableNameColumnTitle(), + sortable: true, + width: '30%', + dataType: 'string', + render: (name: string, template) => { + const templateName = name.length ? name : 'Unnamed Template'; + + return ( + onCreateWorkpad(template)} + aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)} + type="button" + > + {templateName} + + ); + }, + }, + { + field: 'help', + name: strings.getTableDescriptionColumnTitle(), + sortable: false, + dataType: 'string', + width: '30%', + }, + { + field: 'tags', + name: strings.getTableTagsColumnTitle(), + sortable: false, + dataType: 'string', + width: '30%', + render: (tags: string[]) => , + }, + ]; + + let uniqueTagNames: string[] = []; + + templates.forEach((template) => { + const { tags } = template; + tags.forEach((tag) => uniqueTagNames.push(tag)); + uniqueTagNames = uniq(uniqueTagNames); + }); + + const uniqueTags = uniqueTagNames.map( + (name) => + tagsRegistry.get(name) || { + color: undefined, + name, + } + ); + + const filters: SearchFilterConfig[] = [ + { + type: 'field_value_selection', + field: 'tags', + name: 'Tags', + multiSelect: true, + options: uniqueTags.map((tag) => ({ + value: tag.name, + name: tag.name, + view: , + })), + }, + ]; + + const search: EuiSearchBarProps = { + box: { + incremental: true, + schema: true, + }, + filters, + }; + + return ( + + ); +}; + +const strings = { + getCloneTemplateLinkAriaLabel: (templateName: string) => + i18n.translate('xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel', { + defaultMessage: `Clone workpad template '{templateName}'`, + values: { + templateName, + }, + }), + getTableDescriptionColumnTitle: () => + i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', { + defaultMessage: 'Description', + }), + getTableNameColumnTitle: () => + i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', { + defaultMessage: 'Template name', + }), + getTableTagsColumnTitle: () => + i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', { + defaultMessage: 'Tags', + description: + 'This column contains relevant tags that indicate what type of template ' + + 'is displayed. For example: "report", "presentation", etc.', + }), + getTemplateSearchPlaceholder: () => + i18n.translate('xpack.canvas.workpadTemplates.searchPlaceholder', { + defaultMessage: 'Find template', + }), + getCreatingTemplateLabel: (templateName: string) => + i18n.translate('xpack.canvas.workpadTemplates.creatingTemplateLabel', { + defaultMessage: `Creating from template '{templateName}'`, + values: { + templateName, + }, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx new file mode 100644 index 0000000000000..cb2b872ea15f9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiPanel } from '@elastic/eui'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; + +import { + reduxDecorator, + getAddonPanelParameters, + servicesContextDecorator, + getDisableStoryshotsParameter, +} from '../../../../storybook'; +import { getSomeTemplates } from '../../../services/stubs/workpad'; + +import { WorkpadTemplates } from './workpad_templates'; +import { WorkpadTemplates as WorkpadTemplatesComponent } from './workpad_templates.component'; + +export default { + title: 'Home/Workpad Templates', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, +}; + +export const NoTemplates = () => { + return ( + + + + ); +}; + +export const HasTemplates = () => { + return ( + + + + ); +}; + +NoTemplates.decorators = [servicesContextDecorator()]; +HasTemplates.decorators = [servicesContextDecorator({ findTemplates: true })]; + +export const Component = ({ hasTemplates }: { hasTemplates: boolean }) => { + return ( + + + + ); +}; + +Component.args = { + hasTemplates: true, +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx new file mode 100644 index 0000000000000..352285e66424b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx @@ -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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +import { useCreateFromTemplate, useFindTemplatesOnMount } from '../hooks'; + +import { WorkpadTemplates as Component } from './workpad_templates.component'; + +export const WorkpadTemplates = () => { + const [isMounted, templateResponse] = useFindTemplatesOnMount(); + const onCreateWorkpad = useCreateFromTemplate(); + + if (!isMounted) { + return ( + + + + + + ); + } + const { templates } = templateResponse; + + return ; +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default WorkpadTemplates; diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx index 712b06cb39299..2e3e826cc32b5 100644 --- a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx @@ -6,9 +6,7 @@ */ import React, { FC } from 'react'; -import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; -// @ts-expect-error untyped local -import { WorkpadManager } from '../workpad_manager'; +import { Home } from '../home'; // @ts-expect-error untyped local import { setDocTitle } from '../../lib/doc_title'; @@ -19,17 +17,5 @@ export interface Props { export const HomeApp: FC = ({ onLoad = () => {} }) => { onLoad(); setDocTitle('Canvas'); - return ( - - - - {}} /> - - - - ); + return ; }; diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx index e4f297446701c..bd47bb52e0030 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx @@ -18,7 +18,6 @@ storiesOf('components/Toolbar', module) isWriteable={true} selectedPageNumber={1} totalPages={1} - workpadId={'abc'} workpadName={'My Canvas Workpad'} /> )) @@ -28,7 +27,6 @@ storiesOf('components/Toolbar', module) selectedElement={getDefaultElement()} selectedPageNumber={1} totalPages={1} - workpadId={'abc'} workpadName={'My Canvas Workpad'} /> )); diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index baafbdafcc549..9e89ad4c4f27b 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -7,17 +7,8 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiModal, - EuiModalFooter, - EuiButton, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -// @ts-expect-error untyped local -import { WorkpadManager } from '../workpad_manager'; import { PageManager } from '../page_manager'; import { Expression } from '../expression'; import { Tray } from './tray'; @@ -37,7 +28,6 @@ export interface Props { selectedElement?: CanvasElement; selectedPageNumber: number; totalPages: number; - workpadId: string; workpadName: string; } @@ -46,11 +36,9 @@ export const Toolbar: FC = ({ selectedElement, selectedPageNumber, totalPages, - workpadId, workpadName, }) => { const [activeTray, setActiveTray] = useState(null); - const [showWorkpadManager, setShowWorkpadManager] = useState(false); const { getUrl, previousPage } = useContext(WorkpadRoutingContext); // While the tray doesn't get activated if the workpad isn't writeable, @@ -75,20 +63,6 @@ export const Toolbar: FC = ({ } }; - const closeWorkpadManager = () => setShowWorkpadManager(false); - const openWorkpadManager = () => setShowWorkpadManager(true); - - const workpadManager = ( - - - - - {strings.getWorkpadManagerCloseButtonLabel()} - - - - ); - const trays = { pageManager: , expression: !elementIsSelected ? null : setActiveTray(null)} />, @@ -99,12 +73,6 @@ export const Toolbar: FC = ({ {activeTray !== null && setActiveTray(null)}>{trays[activeTray]}}
    - - openWorkpadManager()}> - {workpadName} - - - = ({ )}
    - {showWorkpadManager && workpadManager}
    ); }; @@ -153,6 +120,5 @@ Toolbar.propTypes = { selectedElement: PropTypes.object, selectedPageNumber: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired, - workpadId: PropTypes.string.isRequired, workpadName: PropTypes.string.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx deleted file mode 100644 index 2afd5fe70abe1..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx +++ /dev/null @@ -1,173 +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, { FC, useState, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import moment from 'moment'; -// @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; -import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app'; -import { getWorkpad } from '../../state/selectors/workpad'; -import { getId } from '../../lib/get_id'; -import { downloadWorkpad } from '../../lib/download_workpad'; -import { ComponentStrings, ErrorStrings } from '../../../i18n'; -import { State, CanvasWorkpad } from '../../../types'; -import { useNotifyService, useWorkpadService, usePlatformService } from '../../services'; -// @ts-expect-error -import { WorkpadLoader as Component } from './workpad_loader'; - -const { WorkpadLoader: strings } = ComponentStrings; -const { WorkpadLoader: errors } = ErrorStrings; - -type WorkpadStatePromise = ReturnType['find']>; -type WorkpadState = WorkpadStatePromise extends PromiseLike ? U : never; - -export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { - const fromState = useSelector((state: State) => ({ - workpadId: getWorkpad(state).id, - canUserWrite: canUserWriteSelector(state), - })); - - const [workpadsState, setWorkpadsState] = useState(null); - const workpadService = useWorkpadService(); - const notifyService = useNotifyService(); - const platformService = usePlatformService(); - const history = useHistory(); - - const createWorkpad = useCallback( - async (_workpad: CanvasWorkpad | null | undefined) => { - const workpad = _workpad || getDefaultWorkpad(); - if (workpad != null) { - try { - await workpadService.create(workpad); - history.push(`/workpad/${workpad.id}/page/1`); - } catch (err) { - notifyService.error(err, { - title: errors.getUploadFailureErrorMessage(), - }); - } - return; - } - }, - [workpadService, notifyService, history] - ); - - const findWorkpads = useCallback( - async (text) => { - try { - const fetchedWorkpads = await workpadService.find(text); - setWorkpadsState(fetchedWorkpads); - } catch (err) { - notifyService.error(err, { title: errors.getFindFailureErrorMessage() }); - } - }, - [notifyService, workpadService] - ); - - const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []); - - const cloneWorkpad = useCallback( - async (workpadId: string) => { - try { - const workpad = await workpadService.get(workpadId); - workpad.name = strings.getClonedWorkpadName(workpad.name); - workpad.id = getId('workpad'); - await workpadService.create(workpad); - history.push(`/workpad/${workpad.id}/page/1`); - } catch (err) { - notifyService.error(err, { title: errors.getCloneFailureErrorMessage() }); - } - }, - [notifyService, workpadService, history] - ); - - const removeWorkpads = useCallback( - (workpadIds: string[]) => { - if (workpadsState === null) { - return; - } - - const removedWorkpads = workpadIds.map(async (id) => { - try { - await workpadService.remove(id); - return { id, err: null }; - } catch (err) { - return { id, err }; - } - }); - - return Promise.all(removedWorkpads).then((results) => { - let redirectHome = false; - - const [passes, errored] = results.reduce<[string[], string[]]>( - ([passesArr, errorsArr], result) => { - if (result.id === fromState.workpadId && !result.err) { - redirectHome = true; - } - - if (result.err) { - errorsArr.push(result.id); - } else { - passesArr.push(result.id); - } - - return [passesArr, errorsArr]; - }, - [[], []] - ); - - const remainingWorkpads = workpadsState.workpads.filter(({ id }) => !passes.includes(id)); - - const workpadState = { - total: remainingWorkpads.length, - workpads: remainingWorkpads, - }; - - if (errored.length > 0) { - notifyService.error(errors.getDeleteFailureErrorMessage()); - } - - setWorkpadsState(workpadState); - - if (redirectHome) { - history.push('/'); - } - - return errored; - }); - }, - [history, workpadService, fromState.workpadId, workpadsState, notifyService] - ); - - const formatDate = useCallback( - (date: any) => { - const dateFormat = platformService.getUISetting('dateFormat'); - return date && moment(date).format(dateFormat); - }, - [platformService] - ); - - const { workpadId, canUserWrite } = fromState; - - return ( - - ); -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js b/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js deleted file mode 100644 index 24a694268e4ee..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js +++ /dev/null @@ -1,52 +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 { get } from 'lodash'; -import { getId } from '../../lib/get_id'; -import { ErrorStrings } from '../../../i18n'; - -const { WorkpadFileUpload: errors } = ErrorStrings; - -export const uploadWorkpad = (file, onUpload, notify) => { - if (!file) { - return; - } - - if (get(file, 'type') !== 'application/json') { - return notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { - title: file.name - ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) - : errors.getFileUploadFailureWithoutFileNameErrorMessage(), - }); - } - // TODO: Clean up this file, this loading stuff can, and should be, abstracted - const reader = new FileReader(); - - // handle reading the uploaded file - reader.onload = () => { - try { - const workpad = JSON.parse(reader.result); - workpad.id = getId('workpad'); - - // sanity check for workpad object - if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) { - throw new Error(errors.getMissingPropertiesErrorMessage()); - } - - onUpload(workpad); - } catch (e) { - notify.error(e, { - title: file.name - ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) - : errors.getFileUploadFailureWithoutFileNameErrorMessage(), - }); - } - }; - - // read the uploaded file - reader.readAsText(file); -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js deleted file mode 100644 index 51733dad5b377..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js +++ /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 React from 'react'; -import PropTypes from 'prop-types'; -import { EuiButton } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; - -const { WorkpadCreate: strings } = ComponentStrings; - -export const WorkpadCreate = ({ createPending, onCreate, ...rest }) => ( - - {strings.getWorkpadCreateButtonLabel()} - -); - -WorkpadCreate.propTypes = { - onCreate: PropTypes.func.isRequired, - createPending: PropTypes.bool, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js deleted file mode 100644 index 7c34837771c6f..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js +++ /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 PropTypes from 'prop-types'; -import { compose, withHandlers } from 'recompose'; -import { uploadWorkpad } from '../upload_workpad'; -import { ErrorStrings } from '../../../../i18n'; -import { WorkpadDropzone as Component } from './workpad_dropzone'; - -const { WorkpadFileUpload: errors } = ErrorStrings; - -export const WorkpadDropzone = compose( - withHandlers(({ notify }) => ({ - onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload), - onDropRejected: () => ([file]) => { - notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { - title: file.name - ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) - : errors.getFileUploadFailureWithoutFileNameErrorMessage(), - }); - }, - })) -)(Component); - -WorkpadDropzone.propTypes = { - onUpload: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js deleted file mode 100644 index f77929e1feb76..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js +++ /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 React from 'react'; -import PropTypes from 'prop-types'; -import Dropzone from 'react-dropzone'; - -export const WorkpadDropzone = ({ onDropAccepted, onDropRejected, disabled, children }) => ( - - {children} - -); - -WorkpadDropzone.propTypes = { - onDropAccepted: PropTypes.func.isRequired, - onDropRejected: PropTypes.func.isRequired, - disabled: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss deleted file mode 100644 index ac6838da97fbd..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss +++ /dev/null @@ -1,22 +0,0 @@ -.canvasWorkpad__dropzone { - border: 2px dashed transparent; -} - -.canvasWorkpad__dropzone--active { - background-color: $euiColorLightestShade; - border-color: $euiColorLightShade; -} - -.canvasWorkpad__dropzoneTable .euiTable { - background-color: transparent; -} - -.canvasWorkpad__dropzoneTable--tags { - .euiTableCellContent { - flex-wrap: wrap; - } - - .euiHealth { - width: 100%; - } -} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js deleted file mode 100644 index 9c232ab43ec8d..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ /dev/null @@ -1,426 +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, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - EuiButtonIcon, - EuiPagination, - EuiSpacer, - EuiButton, - EuiToolTip, - EuiEmptyPrompt, - EuiFilePicker, - EuiLink, -} from '@elastic/eui'; -import { orderBy } from 'lodash'; -import { ConfirmModal } from '../confirm_modal'; -import { RoutingLink } from '../routing'; -import { Paginate } from '../paginate'; -import { ComponentStrings } from '../../../i18n'; -import { WorkpadDropzone } from './workpad_dropzone'; -import { WorkpadCreate } from './workpad_create'; -import { WorkpadSearch } from './workpad_search'; -import { uploadWorkpad } from './upload_workpad'; - -const { WorkpadLoader: strings } = ComponentStrings; - -const getDisplayName = (name, workpad, loadedWorkpad) => { - const workpadName = name.length ? name : {workpad.id}; - return workpad.id === loadedWorkpad ? {workpadName} : workpadName; -}; - -export class WorkpadLoader extends React.PureComponent { - static propTypes = { - workpadId: PropTypes.string.isRequired, - canUserWrite: PropTypes.bool.isRequired, - createWorkpad: PropTypes.func.isRequired, - findWorkpads: PropTypes.func.isRequired, - downloadWorkpad: PropTypes.func.isRequired, - cloneWorkpad: PropTypes.func.isRequired, - removeWorkpads: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - workpads: PropTypes.object, - formatDate: PropTypes.func.isRequired, - }; - - state = { - createPending: false, - deletingWorkpad: false, - sortField: '@timestamp', - sortDirection: 'desc', - selectedWorkpads: [], - pageSize: 10, - }; - - async componentDidMount() { - // on component load, kick off the workpad search - this.props.findWorkpads(); - - // keep track of whether or not the component is mounted, to prevent rogue setState calls - this._isMounted = true; - } - - UNSAFE_componentWillReceiveProps(newProps) { - // the workpadId prop will change when a is created or loaded, close the toolbar when it does - const { workpadId, onClose } = this.props; - if (workpadId !== newProps.workpadId) { - onClose(); - } - } - - componentWillUnmount() { - this._isMounted = false; - } - - // create new empty workpad - createWorkpad = async () => { - this.setState({ createPending: true }); - await this.props.createWorkpad(); - this._isMounted && this.setState({ createPending: false }); - }; - - // create new workpad from uploaded JSON - onUpload = async (workpad) => { - this.setState({ createPending: true }); - await this.props.createWorkpad(workpad); - this._isMounted && this.setState({ createPending: false }); - }; - - // clone existing workpad - cloneWorkpad = async (workpad) => { - this.setState({ createPending: true }); - await this.props.cloneWorkpad(workpad.id); - this._isMounted && this.setState({ createPending: false }); - }; - - // Workpad remove methods - openRemoveConfirm = () => this.setState({ deletingWorkpad: true }); - - closeRemoveConfirm = () => this.setState({ deletingWorkpad: false }); - - removeWorkpads = () => { - const { selectedWorkpads } = this.state; - - this.props.removeWorkpads(selectedWorkpads.map(({ id }) => id)).then((remainingIds) => { - const remainingWorkpads = - remainingIds.length > 0 - ? selectedWorkpads.filter(({ id }) => remainingIds.includes(id)) - : []; - - this._isMounted && - this.setState({ - deletingWorkpad: false, - selectedWorkpads: remainingWorkpads, - }); - }); - }; - - // downloads selected workpads as JSON files - downloadWorkpads = () => { - this.state.selectedWorkpads.forEach(({ id }) => this.props.downloadWorkpad(id)); - }; - - onSelectionChange = (selectedWorkpads) => { - this.setState({ selectedWorkpads }); - }; - - onTableChange = ({ sort = {} }) => { - const { field: sortField, direction: sortDirection } = sort; - this.setState({ - sortField, - sortDirection, - }); - }; - - renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }) => { - const { sortField, sortDirection } = this.state; - const { canUserWrite, createPending, workpadId: loadedWorkpad } = this.props; - - const actions = [ - { - render: (workpad) => ( - - - - this.props.downloadWorkpad(workpad.id)} - aria-label={strings.getExportToolTip()} - /> - - - - - this.cloneWorkpad(workpad)} - aria-label={strings.getCloneToolTip()} - disabled={!canUserWrite} - /> - - - - ), - }, - ]; - - const columns = [ - { - field: 'name', - name: strings.getTableNameColumnTitle(), - sortable: true, - dataType: 'string', - render: (name, workpad) => { - const workpadName = getDisplayName(name, workpad, loadedWorkpad); - - return ( - - {workpadName} - - ); - }, - }, - { - field: '@created', - name: strings.getTableCreatedColumnTitle(), - sortable: true, - dataType: 'date', - width: '20%', - render: (date) => this.props.formatDate(date), - }, - { - field: '@timestamp', - name: strings.getTableUpdatedColumnTitle(), - sortable: true, - dataType: 'date', - width: '20%', - render: (date) => this.props.formatDate(date), - }, - { name: strings.getTableActionsColumnTitle(), actions, width: '100px' }, - ]; - - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - const selection = { - itemId: 'id', - onSelectionChange: this.onSelectionChange, - }; - - const emptyTable = ( - {strings.getEmptyPromptTitle()}} - titleSize="s" - body={ - -

    {strings.getEmptyPromptGettingStartedDescription()}

    -

    - {strings.getEmptyPromptNewUserDescription()}{' '} - - {strings.getSampleDataLinkLabel()} - - . -

    -
    - } - /> - ); - - return ( - - - - - {rows.length > 0 && ( - - - - - - )} - - - ); - }; - - render() { - const { - deletingWorkpad, - createPending, - selectedWorkpads, - sortField, - sortDirection, - } = this.state; - const { canUserWrite } = this.props; - const isLoading = this.props.workpads == null; - - let createButton = ( - - ); - - let deleteButton = ( - - {strings.getDeleteButtonLabel(selectedWorkpads.length)} - - ); - - const downloadButton = ( - - {strings.getExportButtonLabel(selectedWorkpads.length)} - - ); - - let uploadButton = ( - uploadWorkpad(file, this.onUpload, this.props.notify)} - accept="application/json" - disabled={createPending || !canUserWrite} - /> - ); - - if (!canUserWrite) { - createButton = ( - {createButton} - ); - deleteButton = ( - {deleteButton} - ); - uploadButton = ( - {uploadButton} - ); - } - - const modalTitle = - selectedWorkpads.length === 1 - ? strings.getDeleteSingleWorkpadModalTitle(selectedWorkpads[0].name) - : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpads.length); - - const confirmModal = ( - - ); - - let sortedWorkpads = []; - - if (!createPending && !isLoading) { - const { workpads } = this.props.workpads; - sortedWorkpads = orderBy(workpads, [sortField, '@timestamp'], [sortDirection, 'desc']); - } - - return ( - - {(pagination) => ( - - - - - {selectedWorkpads.length > 0 && ( - - {downloadButton} - {deleteButton} - - )} - - { - pagination.setPage(0); - this.props.findWorkpads(text); - }} - /> - - - - - - {uploadButton} - {createButton} - - - - - - - {createPending && ( -
    {strings.getCreateWorkpadLoadingDescription()}
    - )} - - {!createPending && isLoading && ( -
    {strings.getFetchLoadingDescription()}
    - )} - - {!createPending && !isLoading && this.renderWorkpadTable(pagination)} - - {confirmModal} -
    - )} -
    - ); - } -} diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss deleted file mode 100644 index 3b2c8eae9e542..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss +++ /dev/null @@ -1,25 +0,0 @@ -.canvasWorkpad__upload--compressed { - - &.euiFilePicker--compressed.euiFilePicker { - .euiFilePicker__prompt { - height: $euiSizeXXL; - padding: $euiSizeM; - padding-left: $euiSizeXXL; - } - - .euiFilePicker__icon { - top: $euiSizeM; - } - } - - // The file picker input is being used moreso as a button, outside of a form, - // and thus the need to override the default max-width of form inputs. - // An issue has been opened in EUI to consider creating a button - // version of the file picker - https://github.com/elastic/eui/issues/1987 - - .euiFilePicker__wrap { - @include euiBreakpoint('xs', 's') { - max-width: none; - } - } -} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js deleted file mode 100644 index 8bf8bbae8ced4..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js +++ /dev/null @@ -1,44 +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 PropTypes from 'prop-types'; -import { EuiFieldSearch } from '@elastic/eui'; -import { debounce } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; - -const { WorkpadSearch: strings } = ComponentStrings; -export class WorkpadSearch extends React.PureComponent { - static propTypes = { - onChange: PropTypes.func.isRequired, - initialText: PropTypes.string, - }; - - state = { - searchText: this.props.initialText || '', - }; - - triggerChange = debounce(this.props.onChange, 150); - - setSearchText = (ev) => { - const text = ev.target.value; - this.setState({ searchText: text }); - this.triggerChange(text); - }; - - render() { - return ( - - ); - } -} diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js deleted file mode 100644 index 8055be32ac481..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js +++ /dev/null @@ -1,69 +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, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiTabbedContent, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { WorkpadLoader } from '../workpad_loader'; -import { WorkpadTemplates } from '../workpad_templates'; -import { ComponentStrings } from '../../../i18n'; - -const { WorkpadManager: strings } = ComponentStrings; - -export const WorkpadManager = ({ onClose }) => { - const tabs = [ - { - id: 'workpadLoader', - name: strings.getMyWorkpadsTabLabel(), - content: ( - - - - - ), - }, - { - id: 'workpadTemplates', - name: strings.getWorkpadTemplatesTabLabel(), - 'data-test-subj': 'workpadTemplates', - content: ( - - - - - ), - }, - ]; - return ( - - - - - -

    {strings.getModalTitle()}

    -
    -
    -
    -
    - - - -
    - ); -}; - -WorkpadManager.propTypes = { - onClose: PropTypes.func, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot deleted file mode 100644 index cab6e8fd9b5f5..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ /dev/null @@ -1,564 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/WorkpadTemplates default 1`] = ` -
    -
    -
    -
    -
    - -
    - - -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - Description - - - - - - Tags - - -
    -
    - Template name -
    -
    - -
    -
    -
    - Description -
    -
    - - This is a test template - -
    -
    -
    - Tags -
    -
    -
    -
    -
    - -
    -
    - tag1 -
    -
    -
    -
    -
    -
    - -
    -
    - tag2 -
    -
    -
    -
    -
    -
    - Template name -
    -
    - -
    -
    -
    - Description -
    -
    - - This is a second test template - -
    -
    -
    - Tags -
    -
    -
    -
    -
    - -
    -
    - tag2 -
    -
    -
    -
    -
    -
    - -
    -
    - tag3 -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -`; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx deleted file mode 100644 index 8e6c055478ca2..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx +++ /dev/null @@ -1,45 +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 { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import { WorkpadTemplates } from '../workpad_templates'; -import { CanvasTemplate } from '../../../../types'; - -const templates: Record = { - test1: { - id: 'test1-id', - name: 'test1', - help: 'This is a test template', - tags: ['tag1', 'tag2'], - template_key: 'test1-key', - }, - test2: { - id: 'test2-id', - name: 'test2', - help: 'This is a second test template', - tags: ['tag2', 'tag3'], - template_key: 'test2-key', - }, -}; - -storiesOf('components/WorkpadTemplates', module) - .addDecorator((story) =>
    {story()}
    ) - .add('default', () => { - const onCreateFromTemplateAction = action('onCreateFromTemplate'); - return ( - { - onCreateFromTemplateAction(template); - return Promise.resolve(); - }} - /> - ); - }); diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx deleted file mode 100644 index 7e007b1253464..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx +++ /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 React, { useCallback, useState, useEffect, FunctionComponent } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; - -import { ComponentStrings } from '../../../i18n/components'; -// @ts-expect-error -import * as workpadService from '../../lib/workpad_service'; -import { WorkpadTemplates as Component } from './workpad_templates'; -import { CanvasTemplate } from '../../../types'; -import { list } from '../../lib/template_service'; -import { applyTemplateStrings } from '../../../i18n/templates/apply_strings'; -import { useNotifyService, useServices } from '../../services'; - -interface WorkpadTemplatesProps { - onClose: () => void; -} - -const Creating: FunctionComponent<{ name: string }> = ({ name }) => ( -
    - {' '} - {ComponentStrings.WorkpadTemplates.getCreatingTemplateLabel(name)} -
    -); -export const WorkpadTemplates: FunctionComponent = ({ onClose }) => { - const history = useHistory(); - const services = useServices(); - - const [templates, setTemplates] = useState(undefined); - const [creatingFromTemplateName, setCreatingFromTemplateName] = useState( - undefined - ); - const { error } = useNotifyService(); - - useEffect(() => { - if (!templates) { - (async () => { - const fetchedTemplates = await list(); - setTemplates(applyTemplateStrings(fetchedTemplates)); - })(); - } - }, [templates]); - - let templateProp: Record = {}; - - if (templates) { - templateProp = templates.reduce>((reduction, template) => { - reduction[template.name] = template; - return reduction; - }, {}); - } - - const createFromTemplate = useCallback( - async (template: CanvasTemplate) => { - setCreatingFromTemplateName(template.name); - try { - const result = await services.workpad.createFromTemplate(template.id); - history.push(`/workpad/${result.id}/page/1`); - } catch (e) { - setCreatingFromTemplateName(undefined); - error(e, { - title: `Couldn't create workpad from template`, - }); - } - }, - [services.workpad, error, history] - ); - - if (creatingFromTemplateName) { - return ; - } - - return ( - - ); -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx deleted file mode 100644 index 72871b93c1735..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx +++ /dev/null @@ -1,215 +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, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - EuiPagination, - EuiSpacer, - EuiButtonEmpty, - EuiSearchBar, - EuiTableSortingType, - Direction, - SortDirection, -} from '@elastic/eui'; -import { orderBy } from 'lodash'; -// @ts-ignore untyped local -import { EuiBasicTableColumn } from '@elastic/eui'; -import { Paginate, PaginateChildProps } from '../paginate'; -import { TagList } from '../tag_list'; -import { getTagsFilter } from '../../lib/get_tags_filter'; -// @ts-expect-error -import { extractSearch } from '../../lib/extract_search'; -import { ComponentStrings } from '../../../i18n'; -import { CanvasTemplate } from '../../../types'; - -interface TableChange { - page?: { - index: number; - size: number; - }; - sort?: { - field: keyof T; - direction: Direction; - }; -} - -const { WorkpadTemplates: strings } = ComponentStrings; - -interface WorkpadTemplatesProps { - onCreateFromTemplate: (template: CanvasTemplate) => Promise; - onClose: () => void; - templates: Record; -} - -interface WorkpadTemplatesState { - sortField: string; - sortDirection: Direction; - pageSize: number; - searchTerm: string; - filterTags: string[]; -} - -export class WorkpadTemplates extends React.PureComponent< - WorkpadTemplatesProps, - WorkpadTemplatesState -> { - static propTypes = { - onCreateFromTemplate: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - templates: PropTypes.object, - }; - - state = { - sortField: 'name', - sortDirection: SortDirection.ASC, - pageSize: 10, - searchTerm: '', - filterTags: [], - }; - - tagType: 'health' = 'health'; - - onTableChange = (tableChange: TableChange) => { - if (tableChange.sort) { - const { field: sortField, direction: sortDirection } = tableChange.sort; - this.setState({ - sortField, - sortDirection, - }); - } - }; - - onSearch = ({ queryText = '' }) => this.setState(extractSearch(queryText)); - - cloneTemplate = (template: CanvasTemplate) => - this.props.onCreateFromTemplate(template).then(() => this.props.onClose()); - - renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }: PaginateChildProps) => { - const { sortField, sortDirection } = this.state; - - const columns: Array> = [ - { - field: 'name', - name: strings.getTableNameColumnTitle(), - sortable: true, - width: '30%', - dataType: 'string', - render: (name: string, template) => { - const templateName = name.length ? name : 'Unnamed Template'; - - return ( - this.cloneTemplate(template)} - aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)} - type="button" - > - {templateName} - - ); - }, - }, - { - field: 'help', - name: strings.getTableDescriptionColumnTitle(), - sortable: false, - dataType: 'string', - width: '30%', - }, - { - field: 'tags', - name: strings.getTableTagsColumnTitle(), - sortable: false, - dataType: 'string', - width: '30%', - render: (tags: string[]) => , - }, - ]; - - const sorting: EuiTableSortingType = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - return ( - - - - {rows.length > 0 && ( - - - - - - )} - - ); - }; - - renderSearch = () => { - const { searchTerm } = this.state; - const filters = [getTagsFilter(this.tagType)]; - - return ( - - ); - }; - - render() { - const { templates } = this.props; - const { sortField, sortDirection, searchTerm, filterTags } = this.state; - const sortedTemplates = orderBy(templates, [sortField, 'name'], [sortDirection, 'asc']); - - const filteredTemplates = sortedTemplates.filter(({ name = '', help = '', tags = [] }) => { - const tagMatch = filterTags.length - ? filterTags.every((filterTag) => tags.indexOf(filterTag) > -1) - : true; - - const lowercaseSearch = searchTerm.toLowerCase(); - const textMatch = lowercaseSearch - ? name.toLowerCase().indexOf(lowercaseSearch) > -1 || - help.toLowerCase().indexOf(lowercaseSearch) > -1 - : true; - - return tagMatch && textMatch; - }); - - return ( - - {(pagination: PaginateChildProps) => ( - - {this.renderSearch()} - - {this.renderWorkpadTable(pagination)} - - )} - - ); - } -} diff --git a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx b/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx deleted file mode 100644 index 12d77c9c7f0c0..0000000000000 --- a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx +++ /dev/null @@ -1,39 +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 { sortBy } from 'lodash'; -import { SearchFilterConfig } from '@elastic/eui'; -import { Tag } from '../components/tag'; -import { getId } from './get_id'; -import { tagsRegistry } from './tags_registry'; -import { ComponentStrings } from '../../i18n'; - -const { WorkpadTemplates: strings } = ComponentStrings; - -// EUI helper function -// generates the FieldValueSelectionFilter object for EuiSearchBar for tag filtering -export const getTagsFilter = (type: 'health' | 'badge'): SearchFilterConfig => { - const uniqueTags = sortBy(Object.values(tagsRegistry.toJS()), 'name'); - const filterType = 'field_value_selection'; - - return { - type: filterType, - field: 'tag', - name: strings.getTableTagsColumnTitle(), - multiSelect: true, - options: uniqueTags.map(({ name, color }) => ({ - value: name, - name, - view: ( -
    - -
    - ), - })), - }; -}; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 6c039660c64c7..3f8f58367171a 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -34,7 +34,7 @@ export type CanvasServiceFactory = ( appUpdater: BehaviorSubject ) => Service | Promise; -class CanvasServiceProvider { +export class CanvasServiceProvider { private factory: CanvasServiceFactory; private service: Service | undefined; diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts index ea80a5a7c26b9..5776a1d0d6983 100644 --- a/x-pack/plugins/canvas/public/services/stubs/platform.ts +++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts @@ -9,13 +9,19 @@ import { PlatformService } from '../platform'; const noop = (..._args: any[]): any => {}; +const uiSettings: Record = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', +}; + +const getUISetting = (setting: string) => uiSettings[setting]; + export const platformService: PlatformService = { getBasePath: () => '/base/path', getBasePathInterface: noop, getDocLinkVersion: () => 'dockLinkVersion', getElasticWebsiteUrl: () => 'https://elastic.co', getHasWriteAccess: () => true, - getUISetting: noop, + getUISetting, setBreadcrumbs: noop, setRecentlyAccessed: noop, getSavedObjects: noop, diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts index 857831c92a8a6..4e3612feb67c8 100644 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -5,17 +5,95 @@ * 2.0. */ +import moment from 'moment'; + +// @ts-expect-error +import { getDefaultWorkpad } from '../../state/defaults'; import { WorkpadService } from '../workpad'; -import { CanvasWorkpad } from '../../../types'; +import { getId } from '../../lib/get_id'; +import { CanvasTemplate } from '../../../types'; -export const workpadService: WorkpadService = { - get: (id: string) => Promise.resolve({} as CanvasWorkpad), - create: (workpad) => Promise.resolve({} as CanvasWorkpad), - createFromTemplate: (templateId: string) => Promise.resolve({} as CanvasWorkpad), - find: (term: string) => - Promise.resolve({ +const TIMEOUT = 500; + +const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time)); +const getName = () => { + const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split( + ' ' + ); + return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' '); +}; + +const randomDate = ( + start: Date = moment().toDate(), + end: Date = moment().subtract(7, 'days').toDate() +) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString(); + +const templates: CanvasTemplate[] = [ + { + id: 'test1-id', + name: 'test1', + help: 'This is a test template', + tags: ['tag1', 'tag2'], + template_key: 'test1-key', + }, + { + id: 'test2-id', + name: 'test2', + help: 'This is a second test template', + tags: ['tag2', 'tag3'], + template_key: 'test2-key', + }, +]; + +export const getSomeWorkpads = (count = 3) => + Array.from({ length: count }, () => ({ + '@created': randomDate( + moment().subtract(3, 'days').toDate(), + moment().subtract(10, 'days').toDate() + ), + '@timestamp': randomDate(), + id: getId('workpad'), + name: getName(), + })); + +export const findSomeWorkpads = (count = 3, timeout = TIMEOUT) => (_term: string) => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => ({ + total: count, + workpads: getSomeWorkpads(count), + })); +}; + +export const findNoWorkpads = (timeout = TIMEOUT) => (_term: string) => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => ({ total: 0, workpads: [], - }), - remove: (id: string) => Promise.resolve(undefined), + })); +}; + +export const findSomeTemplates = (timeout = TIMEOUT) => () => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => getSomeTemplates()); +}; + +export const findNoTemplates = (timeout = TIMEOUT) => () => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => getNoTemplates()); +}; + +export const getNoTemplates = () => ({ templates: [] }); +export const getSomeTemplates = () => ({ templates }); + +export const workpadService: WorkpadService = { + get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }), + findTemplates: findNoTemplates(), + create: (workpad) => Promise.resolve(workpad), + createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()), + find: findNoWorkpads(), + remove: (id: string) => Promise.resolve(), }; diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index 11690ca4c0c45..7d2f1550a312f 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { API_ROUTE_WORKPAD, DEFAULT_WORKPAD_CSS } from '../../common/lib/constants'; -import { CanvasWorkpad } from '../../types'; +import { + API_ROUTE_WORKPAD, + DEFAULT_WORKPAD_CSS, + API_ROUTE_TEMPLATES, +} from '../../common/lib/constants'; +import { CanvasWorkpad, CanvasTemplate } from '../../types'; import { CanvasServiceFactory } from './'; /* @@ -40,9 +44,15 @@ const sanitizeWorkpad = function (workpad: CanvasWorkpad) { return workpad; }; -interface WorkpadFindResponse { +export type FoundWorkpads = Array>; +export type FoundWorkpad = FoundWorkpads[number]; +export interface WorkpadFindResponse { total: number; - workpads: Array>; + workpads: FoundWorkpads; +} + +export interface TemplateFindResponse { + templates: CanvasTemplate[]; } export interface WorkpadService { @@ -51,6 +61,7 @@ export interface WorkpadService { createFromTemplate: (templateId: string) => Promise; find: (term: string) => Promise; remove: (id: string) => Promise; + findTemplates: () => Promise; } export const workpadServiceFactory: CanvasServiceFactory = ( @@ -82,7 +93,9 @@ export const workpadServiceFactory: CanvasServiceFactory = ( body: JSON.stringify({ templateId }), }); }, + findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES), find: (searchTerm: string) => { + // TODO: this shouldn't be necessary. Check for usage. const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0; return coreStart.http.get(`${getApiPath()}/find`, { diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index a79e07a7d0016..d9592d5c0be5f 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -40,8 +40,6 @@ @import '../components/workpad_header/element_menu/element_menu'; @import '../components/workpad_header/share_menu/share_menu'; @import '../components/workpad_header/view_menu/view_menu'; -@import '../components/workpad_loader/workpad_loader'; -@import '../components/workpad_loader/workpad_dropzone/workpad_dropzone'; @import '../components/workpad_page/workpad_page'; @import '../components/workpad_page/workpad_interactive_page/workpad_interactive_page'; @import '../components/workpad_page/workpad_static_page/workpad_static_page'; diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts index a674eaad576a7..598a2333be554 100644 --- a/x-pack/plugins/canvas/storybook/decorators/index.ts +++ b/x-pack/plugins/canvas/storybook/decorators/index.ts @@ -11,6 +11,7 @@ import { kibanaContextDecorator } from './kibana_decorator'; import { servicesContextDecorator } from './services_decorator'; export { reduxDecorator } from './redux_decorator'; +export { servicesContextDecorator } from './services_decorator'; export const addDecorators = () => { if (process.env.NODE_ENV === 'test') { @@ -20,5 +21,5 @@ export const addDecorators = () => { addDecorator(kibanaContextDecorator); addDecorator(routerContextDecorator); - addDecorator(servicesContextDecorator); + addDecorator(servicesContextDecorator()); }; diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx index 01d96cb0c70e6..289171f136ab5 100644 --- a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx @@ -25,7 +25,7 @@ elementsRegistry.register(image); import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state'; export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants'; -interface Params { +export interface Params { workpad?: CanvasWorkpad; elements?: CanvasElement[]; assets?: CanvasAsset[]; diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx index a11492387ea7f..def5a5681a8c4 100644 --- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -7,8 +7,40 @@ import React from 'react'; -import { ServicesProvider } from '../../public/services'; +import { + CanvasServiceFactory, + CanvasServiceProvider, + ServicesProvider, +} from '../../public/services'; +import { + findNoWorkpads, + findSomeWorkpads, + workpadService, + findSomeTemplates, + findNoTemplates, +} from '../../public/services/stubs/workpad'; +import { WorkpadService } from '../../public/services/workpad'; -export const servicesContextDecorator = (story: Function) => ( - {story()} -); +interface Params { + findWorkpads?: number; + findTemplates?: boolean; +} + +export const servicesContextDecorator = ({ + findWorkpads = 0, + findTemplates: findTemplatesOption = false, +}: Params = {}) => { + const workpadServiceFactory: CanvasServiceFactory = (): WorkpadService => ({ + ...workpadService, + find: findWorkpads > 0 ? findSomeWorkpads(findWorkpads) : findNoWorkpads(), + findTemplates: findTemplatesOption ? findSomeTemplates() : findNoTemplates(), + }); + + const workpad = new CanvasServiceProvider(workpadServiceFactory); + // @ts-expect-error This is a hack at the moment, until we can get Canvas moved over to the new services architecture. + workpad.start(); + + return (story: Function) => ( + {story()} + ); +}; diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts index 148af337d7720..ff60b84c88a69 100644 --- a/x-pack/plugins/canvas/storybook/index.ts +++ b/x-pack/plugins/canvas/storybook/index.ts @@ -10,3 +10,8 @@ import { ACTIONS_PANEL_ID } from './addon/src/constants'; export * from './decorators'; export { ACTIONS_PANEL_ID } from './addon/src/constants'; export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } }); +export const getDisableStoryshotsParameter = () => ({ + storyshots: { + disable: true, + }, +}); diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts index 80a8aeb14a804..69c05322cf3f0 100644 --- a/x-pack/plugins/canvas/storybook/main.ts +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -53,6 +53,11 @@ const canvasWebpack = { }, ], }, + resolve: { + alias: { + 'src/plugins': resolve(KIBANA_ROOT, 'src/plugins'), + }, + }, }; module.exports = { diff --git a/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot new file mode 100644 index 0000000000000..39ec1e234ead5 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Home/Empty Prompt Empty Prompt 1`] = ` +
    +
    + +`; diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index 0c3765812066e..7f0ea077c7569 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -90,6 +90,11 @@ import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer' jest.mock('@elastic/eui/test-env/components/observer/observer'); EuiObserver.mockImplementation(() => 'EuiObserver'); +// @ts-expect-error untyped library +import Dropzone from 'react-dropzone'; +jest.mock('react-dropzone'); +Dropzone.mockImplementation(() => 'Dropzone'); + // This element uses a `ref` and cannot be rendered by Jest snapshots. import { RenderedElement } from '../shareable_runtime/components/rendered_element'; jest.mock('../shareable_runtime/components/rendered_element'); @@ -111,7 +116,7 @@ addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots initStoryshots({ - configPath: path.resolve(__dirname, './../storybook'), + configPath: path.resolve(__dirname), framework: 'react', test: multiSnapshotWithOptions({}), // Don't snapshot tests that start with 'redux' diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ce976766e7341..955e4f890e511 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6113,18 +6113,18 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "Elasticsearch インデックスを取得できませんでした", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "「{functionName}」のレンダリングが失敗しました", "xpack.canvas.error.repeatImage.missingMaxArgument": "{emptyImageArgument} を指定する場合は、{maxArgument} を設定する必要があります", - "xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした", - "xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした", - "xpack.canvas.error.workpadLoader.findFailureErrorMessage": "ワークパッドが見つかりませんでした", - "xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", + "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした", + "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", + "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした", + "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "ワークパッドが見つかりませんでした", + "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした", + "xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした", + "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "ワークパッドを作成できませんでした", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "ID でワークパッドを読み込めませんでした", - "xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした", - "xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした", - "xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。", "xpack.canvas.errorComponent.description": "表現が失敗し次のメッセージが返されました:", "xpack.canvas.errorComponent.title": "おっと!表現が失敗しました", - "xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした", + "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした", "xpack.canvas.expression.cancelButtonLabel": "キャンセル", "xpack.canvas.expression.closeButtonLabel": "閉じる", "xpack.canvas.expression.learnLinkText": "表現構文の詳細", @@ -6458,6 +6458,12 @@ "xpack.canvas.helpMenu.description": "{CANVAS} に関する情報", "xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} ドキュメント", "xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "キーボードショートカット", + "xpack.canvas.home.myWorkpadsTabLabel": "マイワークパッド", + "xpack.canvas.home.workpadTemplatesTabLabel": "テンプレート", + "xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。", + "xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合", + "xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "初の’ワークパッドを追加しましょう", + "xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう", "xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前に移動", "xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "表面に移動", "xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "クローンを作成", @@ -6904,6 +6910,7 @@ "xpack.canvas.units.quickRange.last90Days": "過去90日間", "xpack.canvas.units.quickRange.today": "今日", "xpack.canvas.units.quickRange.yesterday": "昨日", + "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} のコピー", "xpack.canvas.varConfig.addButtonLabel": "変数の追加", "xpack.canvas.varConfig.addTooltipLabel": "変数の追加", "xpack.canvas.varConfig.copyActionButtonLabel": "スニペットをコピー", @@ -7030,40 +7037,30 @@ "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム", "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット", "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", - "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} のコピー", - "xpack.canvas.workpadLoader.cloneTooltip": "ワークパッドのクローンを作成します", - "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "ワークパッドを作成中...", - "xpack.canvas.workpadLoader.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除", - "xpack.canvas.workpadLoader.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除", - "xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "削除", - "xpack.canvas.workpadLoader.deleteModalDescription": "削除されたワークパッドは復元できません。", - "xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?", - "xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?", - "xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。", - "xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合", - "xpack.canvas.workpadLoader.emptyPromptTitle": "初の’ワークパッドを追加しましょう", - "xpack.canvas.workpadLoader.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート", - "xpack.canvas.workpadLoader.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ", - "xpack.canvas.workpadLoader.exportTooltip": "ワークパッドをエクスポート", - "xpack.canvas.workpadLoader.fetchLoadingDescription": "ワークパッドを取得中...", - "xpack.canvas.workpadLoader.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート", - "xpack.canvas.workpadLoader.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む", - "xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません", - "xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません", - "xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません", - "xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません", - "xpack.canvas.workpadLoader.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう", - "xpack.canvas.workpadLoader.table.actionsColumnTitle": "アクション", - "xpack.canvas.workpadLoader.table.createdColumnTitle": "作成済み", - "xpack.canvas.workpadLoader.table.nameColumnTitle": "ワークパッド名", - "xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新しました", - "xpack.canvas.workpadManager.modalTitle": "{CANVAS} ワークパッド", - "xpack.canvas.workpadManager.myWorkpadsTabLabel": "マイワークパッド", - "xpack.canvas.workpadManager.workpadTemplatesTabLabel": "テンプレート", - "xpack.canvas.workpadSearch.searchPlaceholder": "ワークパッドを検索", - "xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成", - "xpack.canvas.workpadTemplate.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています", - "xpack.canvas.workpadTemplate.searchPlaceholder": "テンプレートを検索", + "xpack.canvas.workpadImport.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート", + "xpack.canvas.workpadTable.cloneTooltip": "ワークパッドのクローンを作成します", + "xpack.canvas.workpadTable.exportTooltip": "ワークパッドをエクスポート", + "xpack.canvas.workpadTable.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む", + "xpack.canvas.workpadTable.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません", + "xpack.canvas.workpadTable.searchPlaceholder": "ワークパッドを検索", + "xpack.canvas.workpadTable.table.actionsColumnTitle": "アクション", + "xpack.canvas.workpadTable.table.createdColumnTitle": "作成済み", + "xpack.canvas.workpadTable.table.nameColumnTitle": "ワークパッド名", + "xpack.canvas.workpadTable.table.updatedColumnTitle": "更新しました", + "xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除", + "xpack.canvas.workpadTableTools.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除", + "xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "削除", + "xpack.canvas.workpadTableTools.deleteModalDescription": "削除されたワークパッドは復元できません。", + "xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?", + "xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?", + "xpack.canvas.workpadTableTools.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート", + "xpack.canvas.workpadTableTools.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ", + "xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません", + "xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません", + "xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません", + "xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成", + "xpack.canvas.workpadTemplates.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています", + "xpack.canvas.workpadTemplates.searchPlaceholder": "テンプレートを検索", "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "説明", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6796ea0e40c21..c629b1495a602 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6152,18 +6152,18 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "无法提取 Elasticsearch 索引", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "呈现“{functionName}”失败。", "xpack.canvas.error.repeatImage.missingMaxArgument": "如果提供 {emptyImageArgument},则必须设置 {maxArgument}", - "xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "无法克隆 Workpad", - "xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "无法删除所有 Workpad", - "xpack.canvas.error.workpadLoader.findFailureErrorMessage": "无法查找 Workpad", - "xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "无法上传 Workpad", + "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "无法克隆 Workpad", + "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", + "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "无法删除所有 Workpad", + "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "无法查找 Workpad", + "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件", + "xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件", + "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "无法创建 Workpad", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "无法加载具有以下 ID 的 Workpad", - "xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件", - "xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件", - "xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。", "xpack.canvas.errorComponent.description": "表达式失败,并显示消息:", "xpack.canvas.errorComponent.title": "哎哟!表达式失败", - "xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”", + "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”", "xpack.canvas.expression.cancelButtonLabel": "取消", "xpack.canvas.expression.closeButtonLabel": "关闭", "xpack.canvas.expression.learnLinkText": "学习表达式语法", @@ -6498,6 +6498,12 @@ "xpack.canvas.helpMenu.description": "有关 {CANVAS} 特定信息", "xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} 文档", "xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "快捷键", + "xpack.canvas.home.myWorkpadsTabLabel": "我的 Workpad", + "xpack.canvas.home.workpadTemplatesTabLabel": "模板", + "xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。", + "xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} 新手?", + "xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "添加您的首个 Workpad", + "xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "添加您的首个 Workpad", "xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前移", "xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "置前", "xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "克隆", @@ -6948,6 +6954,7 @@ "xpack.canvas.units.time.hours": "{hours, plural, other {# 小时}}", "xpack.canvas.units.time.minutes": "{minutes, plural, other {# 分钟}}", "xpack.canvas.units.time.seconds": "{seconds, plural, other {# 秒}}", + "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} 副本", "xpack.canvas.varConfig.addButtonLabel": "添加变量", "xpack.canvas.varConfig.addTooltipLabel": "添加变量", "xpack.canvas.varConfig.copyActionButtonLabel": "复制代码片段", @@ -7078,40 +7085,30 @@ "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放", "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置", "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", - "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} 副本", - "xpack.canvas.workpadLoader.cloneTooltip": "克隆 Workpad", - "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "正在创建 Workpad......", - "xpack.canvas.workpadLoader.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad", - "xpack.canvas.workpadLoader.deleteButtonLabel": "删除 ({numberOfWorkpads})", - "xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "删除", - "xpack.canvas.workpadLoader.deleteModalDescription": "您无法恢复删除的 Workpad。", - "xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad?", - "xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?", - "xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。", - "xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} 新手?", - "xpack.canvas.workpadLoader.emptyPromptTitle": "添加您的首个 Workpad", - "xpack.canvas.workpadLoader.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad", - "xpack.canvas.workpadLoader.exportButtonLabel": "导出 ({numberOfWorkpads})", - "xpack.canvas.workpadLoader.exportTooltip": "导出 Workpad", - "xpack.canvas.workpadLoader.fetchLoadingDescription": "正在获取 Workpad......", - "xpack.canvas.workpadLoader.filePickerPlaceholder": "导入 Workpad {JSON} 文件", - "xpack.canvas.workpadLoader.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”", - "xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "您无权克隆 Workpad", - "xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "您无权创建 Workpad", - "xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "您无权删除 Workpad", - "xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "您无权上传 Workpad", - "xpack.canvas.workpadLoader.sampleDataLinkLabel": "添加您的首个 Workpad", - "xpack.canvas.workpadLoader.table.actionsColumnTitle": "操作", - "xpack.canvas.workpadLoader.table.createdColumnTitle": "创建时间", - "xpack.canvas.workpadLoader.table.nameColumnTitle": "Workpad 名称", - "xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新时间", - "xpack.canvas.workpadManager.modalTitle": "{CANVAS} Workpad", - "xpack.canvas.workpadManager.myWorkpadsTabLabel": "我的 Workpad", - "xpack.canvas.workpadManager.workpadTemplatesTabLabel": "模板", - "xpack.canvas.workpadSearch.searchPlaceholder": "查找 Workpad", - "xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”", - "xpack.canvas.workpadTemplate.creatingTemplateLabel": "正在从模板“{templateName}”创建", - "xpack.canvas.workpadTemplate.searchPlaceholder": "查找模板", + "xpack.canvas.workpadImport.filePickerPlaceholder": "导入 Workpad {JSON} 文件", + "xpack.canvas.workpadTable.searchPlaceholder": "查找 Workpad", + "xpack.canvas.workpadTable.cloneTooltip": "克隆 Workpad", + "xpack.canvas.workpadTable.exportTooltip": "导出 Workpad", + "xpack.canvas.workpadTable.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”", + "xpack.canvas.workpadTable.noPermissionToCloneToolTip": "您无权克隆 Workpad", + "xpack.canvas.workpadTable.table.actionsColumnTitle": "操作", + "xpack.canvas.workpadTable.table.createdColumnTitle": "创建时间", + "xpack.canvas.workpadTable.table.nameColumnTitle": "Workpad 名称", + "xpack.canvas.workpadTable.table.updatedColumnTitle": "更新时间", + "xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad", + "xpack.canvas.workpadTableTools.deleteButtonLabel": "删除 ({numberOfWorkpads})", + "xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "删除", + "xpack.canvas.workpadTableTools.deleteModalDescription": "您无法恢复删除的 Workpad。", + "xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad?", + "xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?", + "xpack.canvas.workpadTableTools.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad", + "xpack.canvas.workpadTableTools.exportButtonLabel": "导出 ({numberOfWorkpads})", + "xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "您无权创建 Workpad", + "xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "您无权删除 Workpad", + "xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "您无权上传 Workpad", + "xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”", + "xpack.canvas.workpadTemplates.creatingTemplateLabel": "正在从模板“{templateName}”创建", + "xpack.canvas.workpadTemplates.searchPlaceholder": "查找模板", "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "描述", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签", diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/canvas.ts index a79fb7b60e76a..609c8bf5bb1ae 100644 --- a/x-pack/test/accessibility/apps/canvas.ts +++ b/x-pack/test/accessibility/apps/canvas.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('loads workpads', async function () { await retry.waitFor( 'canvas workpads visible', - async () => await testSubjects.exists('canvasWorkpadLoaderTable') + async () => await testSubjects.exists('canvasWorkpadTable') ); await a11y.testAppSnapshot(); }); diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index 5280ad0118fba..fcc04aafdbcd8 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -17,7 +17,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { describe('smoke test', function () { this.tags('includeFirefox'); - const workpadListSelector = 'canvasWorkpadLoaderTable > canvasWorkpadLoaderWorkpad'; + const workpadListSelector = 'canvasWorkpadTable > canvasWorkpadTableWorkpad'; const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31'; before(async () => { diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index 0e0203046fd16..df92c1c398d93 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -39,7 +39,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo * to load the workpad. Resolves once the workpad is in the DOM */ async loadFirstWorkpad(workpadName: string) { - const elem = await testSubjects.find('canvasWorkpadLoaderWorkpad'); + const elem = await testSubjects.find('canvasWorkpadTableWorkpad'); const text = await elem.getVisibleText(); expect(text).to.be(workpadName); await elem.click(); From 45fcef1603696ae3fc67530fa3bfc31018118419 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 22 Jun 2021 17:22:34 -0400 Subject: [PATCH 080/118] [actions] add rule saved object reference to action execution event log doc (#101526) (#102994) resolves https://github.com/elastic/kibana/issues/99225 Prior to this PR, when an alerting connection action was executed, the event log document generated did not contain a reference to the originating rule. This makes it difficult to diagnose problems with connector errors, since the error is often in the parameters specified in the actions in the alert. In this PR, a reference to the alerting rule is added to the saved_objects field in the event document for these events. --- .../actions/server/actions_client.test.ts | 64 ++++++++++++++ .../plugins/actions/server/actions_client.ts | 9 +- .../server/create_execute_function.test.ts | 56 ++++++++++++ .../actions/server/create_execute_function.ts | 5 +- .../actions/server/lib/action_executor.ts | 13 +++ .../server/lib/related_saved_objects.test.ts | 86 +++++++++++++++++++ .../server/lib/related_saved_objects.ts | 31 +++++++ .../server/lib/task_runner_factory.test.ts | 76 ++++++++++++++++ .../actions/server/lib/task_runner_factory.ts | 4 +- .../actions/server/routes/execute.test.ts | 2 + .../plugins/actions/server/routes/execute.ts | 1 + .../server/routes/legacy/execute.test.ts | 2 + .../actions/server/routes/legacy/execute.ts | 1 + .../server/saved_objects/mappings.json | 4 + .../create_execution_handler.test.ts | 32 +++++++ .../task_runner/create_execution_handler.ts | 12 ++- .../server/task_runner/task_runner.test.ts | 32 +++++++ x-pack/plugins/event_log/README.md | 29 ++++--- 18 files changed, 442 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/related_saved_objects.test.ts create mode 100644 x-pack/plugins/actions/server/lib/related_saved_objects.ts diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 3b91b07eb30f4..16388b2faf52e 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -1676,6 +1676,70 @@ describe('execute()', () => { name: 'my name', }, }); + + await expect( + actionsClient.execute({ + actionId, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + }, + ], + }) + ).resolves.toMatchObject({ status: 'ok', actionId }); + + expect(actionExecutor.execute).toHaveBeenCalledWith({ + actionId, + request, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + }, + ], + }); + + await expect( + actionsClient.execute({ + actionId, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + namespace: 'some-namespace', + }, + ], + }) + ).resolves.toMatchObject({ status: 'ok', actionId }); + + expect(actionExecutor.execute).toHaveBeenCalledWith({ + actionId, + request, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + namespace: 'some-namespace', + }, + ], + }); }); }); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 449d218ed5ae0..f8d13cdafa755 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -469,6 +469,7 @@ export class ActionsClient { actionId, params, source, + relatedSavedObjects, }: Omit): Promise> { if ( (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === @@ -476,7 +477,13 @@ export class ActionsClient { ) { await this.authorization.ensureAuthorized('execute'); } - return this.actionExecutor.execute({ actionId, params, source, request: this.request }); + return this.actionExecutor.execute({ + actionId, + params, + source, + request: this.request, + relatedSavedObjects, + }); } public async enqueueExecution(options: EnqueueExecutionOptions): Promise { diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 4cacba6dc880a..ee8064d2aadc5 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -83,6 +83,62 @@ describe('execute()', () => { }); }); + test('schedules the action with all given parameters and relatedSavedObjects', async () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); + const executeFn = createExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry, + isESOCanEncrypt: true, + preconfiguredActions: [], + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + await executeFn(savedObjectsClient, { + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: Buffer.from('123:abc').toString('base64'), + source: asHttpRequestExecutionSource(request), + relatedSavedObjects: [ + { + id: 'some-id', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'action_task_params', + { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + relatedSavedObjects: [ + { + id: 'some-id', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }, + {} + ); + }); + test('schedules the action with all given parameters with a preconfigured action', async () => { const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 4f3ffbef36c6e..7dcd66c711bdd 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -11,6 +11,7 @@ import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './ty import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; import { isSavedObjectExecutionSource } from './lib'; +import { RelatedSavedObjects } from './lib/related_saved_objects'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -23,6 +24,7 @@ export interface ExecuteOptions extends Pick { request: KibanaRequest; params: Record; source?: ActionExecutionSource; + relatedSavedObjects?: RelatedSavedObjects; } export type ActionExecutorContract = PublicMethodsOf; @@ -68,6 +70,7 @@ export class ActionExecutor { params, request, source, + relatedSavedObjects, }: ExecuteOptions): Promise> { if (!this.isInitialized) { throw new Error('ActionExecutor not initialized'); @@ -154,6 +157,16 @@ export class ActionExecutor { }, }; + for (const relatedSavedObject of relatedSavedObjects || []) { + event.kibana?.saved_objects?.push({ + rel: SAVED_OBJECT_REL_PRIMARY, + type: relatedSavedObject.type, + id: relatedSavedObject.id, + type_id: relatedSavedObject.typeId, + namespace: relatedSavedObject.namespace, + }); + } + eventLogger.startTiming(event); let rawResult: ActionTypeExecutorResult; try { diff --git a/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts new file mode 100644 index 0000000000000..8fd13d1375697 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validatedRelatedSavedObjects } from './related_saved_objects'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { Logger } from '../../../../../src/core/server'; + +const loggerMock = loggingSystemMock.createLogger(); + +describe('related_saved_objects', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('validates valid objects', () => { + ensureValid(loggerMock, undefined); + ensureValid(loggerMock, []); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + typeId: 'some-type-id', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + namespace: 'some-namespace', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + typeId: 'some-type-id', + namespace: 'some-namespace', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + }, + { + id: 'some-id-2', + type: 'some-type-2', + }, + ]); + }); +}); + +it('handles invalid objects', () => { + ensureInvalid(loggerMock, 42); + ensureInvalid(loggerMock, {}); + ensureInvalid(loggerMock, [{}]); + ensureInvalid(loggerMock, [{ id: 'some-id' }]); + ensureInvalid(loggerMock, [{ id: 42 }]); + ensureInvalid(loggerMock, [{ id: 'some-id', type: 'some-type', x: 42 }]); +}); + +function ensureValid(logger: Logger, savedObjects: unknown) { + const result = validatedRelatedSavedObjects(logger, savedObjects); + expect(result).toEqual(savedObjects === undefined ? [] : savedObjects); + expect(loggerMock.warn).not.toHaveBeenCalled(); +} + +function ensureInvalid(logger: Logger, savedObjects: unknown) { + const result = validatedRelatedSavedObjects(logger, savedObjects); + expect(result).toEqual([]); + + const message = loggerMock.warn.mock.calls[0][0]; + expect(message).toMatch( + /ignoring invalid related saved objects: expected value of type \[array\] but got/ + ); +} diff --git a/x-pack/plugins/actions/server/lib/related_saved_objects.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.ts new file mode 100644 index 0000000000000..160587a3a9a8b --- /dev/null +++ b/x-pack/plugins/actions/server/lib/related_saved_objects.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 { schema, TypeOf } from '@kbn/config-schema'; +import { Logger } from '../../../../../src/core/server'; + +export type RelatedSavedObjects = TypeOf; + +const RelatedSavedObjectsSchema = schema.arrayOf( + schema.object({ + namespace: schema.maybe(schema.string({ minLength: 1 })), + id: schema.string({ minLength: 1 }), + type: schema.string({ minLength: 1 }), + // optional; for SO types like action/alert that have type id's + typeId: schema.maybe(schema.string({ minLength: 1 })), + }), + { defaultValue: [] } +); + +export function validatedRelatedSavedObjects(logger: Logger, data: unknown): RelatedSavedObjects { + try { + return RelatedSavedObjectsSchema.validate(data); + } catch (err) { + logger.warn(`ignoring invalid related saved objects: ${err.message}`); + return []; + } +} diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 229324c1f0df3..2292994e3ccfd 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -126,6 +126,7 @@ test('executes the task by calling the executor with proper parameters', async ( expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, + relatedSavedObjects: [], request: expect.objectContaining({ headers: { // base64 encoded "123:abc" @@ -247,6 +248,7 @@ test('uses API key when provided', async () => { expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, + relatedSavedObjects: [], request: expect.objectContaining({ headers: { // base64 encoded "123:abc" @@ -262,6 +264,79 @@ test('uses API key when provided', async () => { ); }); +test('uses relatedSavedObjects when provided', async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + relatedSavedObjects: [{ id: 'some-id', type: 'some-type' }], + }, + references: [], + }); + + await taskRunner.run(); + + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + relatedSavedObjects: [ + { + id: 'some-id', + type: 'some-type', + }, + ], + request: expect.objectContaining({ + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', + }, + }), + }); +}); + +test('sanitizes invalid relatedSavedObjects when provided', async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + relatedSavedObjects: [{ Xid: 'some-id', type: 'some-type' }], + }, + references: [], + }); + + await taskRunner.run(); + + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + relatedSavedObjects: [], + request: expect.objectContaining({ + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', + }, + }), + }); +}); + test(`doesn't use API key when not provided`, async () => { const factory = new TaskRunnerFactory(mockedActionExecutor); factory.initialize(taskRunnerFactoryInitializerParams); @@ -284,6 +359,7 @@ test(`doesn't use API key when not provided`, async () => { expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, + relatedSavedObjects: [], request: expect.objectContaining({ headers: {}, }), diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index cf4b1576f2778..0515963ab82f4 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -30,6 +30,7 @@ import { } from '../types'; import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../constants/saved_objects'; import { asSavedObjectExecutionSource } from './action_execution_source'; +import { validatedRelatedSavedObjects } from './related_saved_objects'; export interface TaskRunnerContext { logger: Logger; @@ -77,7 +78,7 @@ export class TaskRunnerFactory { const namespace = spaceIdToNamespace(spaceId); const { - attributes: { actionId, params, apiKey }, + attributes: { actionId, params, apiKey, relatedSavedObjects }, references, } = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, @@ -117,6 +118,7 @@ export class TaskRunnerFactory { actionId, request: fakeRequest, ...getSourceFromReferences(references), + relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects), }); } catch (e) { if (e instanceof ActionTypeDisabledError) { diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 4b12bf3111c1f..54e10698e5af9 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -65,6 +65,7 @@ describe('executeActionRoute', () => { someData: 'data', }, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); expect(res.ok).toHaveBeenCalled(); @@ -101,6 +102,7 @@ describe('executeActionRoute', () => { expect(actionsClient.execute).toHaveBeenCalledWith({ actionId: '1', params: {}, + relatedSavedObjects: [], source: asHttpRequestExecutionSource(req), }); diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 377fe1215b3fb..7e8110365e87a 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -53,6 +53,7 @@ export const executeActionRoute = ( params, actionId: id, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); return body ? res.ok({ diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts index 2ac53ddaaedf6..05b71819911a3 100644 --- a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts @@ -63,6 +63,7 @@ describe('executeActionRoute', () => { someData: 'data', }, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); expect(res.ok).toHaveBeenCalled(); @@ -100,6 +101,7 @@ describe('executeActionRoute', () => { actionId: '1', params: {}, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); expect(res.ok).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.ts b/x-pack/plugins/actions/server/routes/legacy/execute.ts index f6ddec1d01c20..d7ed8d2e15604 100644 --- a/x-pack/plugins/actions/server/routes/legacy/execute.ts +++ b/x-pack/plugins/actions/server/routes/legacy/execute.ts @@ -48,6 +48,7 @@ export const executeActionRoute = ( params, actionId: id, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); return body ? res.ok({ diff --git a/x-pack/plugins/actions/server/saved_objects/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json index c598b96ba2451..57f801ae9a075 100644 --- a/x-pack/plugins/actions/server/saved_objects/mappings.json +++ b/x-pack/plugins/actions/server/saved_objects/mappings.json @@ -35,6 +35,10 @@ }, "apiKey": { "type": "binary" + }, + "relatedSavedObjects": { + "enabled": false, + "type": "object" } } } diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 25f0656163f5d..033ffcceb6a0a 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -135,6 +135,14 @@ test('enqueues execution per selected action', async () => { "foo": true, "stateVal": "My goes here", }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -247,6 +255,14 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => id: '1', type: 'alert', }), + relatedSavedObjects: [ + { + id: '1', + namespace: 'test1', + type: 'alert', + typeId: 'test', + }, + ], spaceId: 'test1', apiKey: createExecutionHandlerParams.apiKey, }); @@ -327,6 +343,14 @@ test('context attribute gets parameterized', async () => { "foo": true, "stateVal": "My goes here", }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -360,6 +384,14 @@ test('state attribute gets parameterized', async () => { "foo": true, "stateVal": "My state-val goes here", }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index c3a36297c217a..968fff540dc03 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -157,6 +157,8 @@ export function createExecutionHandler< continue; } + const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; + // TODO would be nice to add the action name here, but it's not available const actionLabel = `${action.actionTypeId}:${action.id}`; const actionsClient = await actionsPlugin.getActionsClientWithRequest(request); @@ -169,10 +171,16 @@ export function createExecutionHandler< id: alertId, type: 'alert', }), + relatedSavedObjects: [ + { + id: alertId, + type: 'alert', + namespace: namespace.namespace, + typeId: alertType.id, + }, + ], }); - const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; - const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.executeAction, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 39a45584631d2..8ab267a5610d3 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -352,6 +352,14 @@ describe('Task Runner', () => { "params": Object { "foo": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -1098,6 +1106,14 @@ describe('Task Runner', () => { "params": Object { "foo": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -1634,6 +1650,14 @@ describe('Task Runner', () => { "params": Object { "isResolved": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -1826,6 +1850,14 @@ describe('Task Runner', () => { "params": Object { "isResolved": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 032f77543acb9..ffbd20dd6f2be 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -131,7 +131,7 @@ Below is a document in the expected structure, with descriptions of the fields: instance_id: "alert instance id, for relevant documents", action_group_id: "alert action group, for relevant documents", action_subgroup: "alert action subgroup, for relevant documents", - status: "overall alert status, after alert execution", + status: "overall alert status, after rule execution", }, saved_objects: [ { @@ -160,21 +160,26 @@ plugins: - `action: execute-via-http` - generated when an action is executed via HTTP request - `provider: alerting` - - `action: execute` - generated when an alert executor runs - - `action: execute-action` - generated when an alert schedules an action to run - - `action: new-instance` - generated when an alert has a new instance id that is active - - `action: recovered-instance` - generated when an alert has a previously active instance id that is no longer active - - `action: active-instance` - generated when an alert determines an instance id is active + - `action: execute` - generated when a rule executor runs + - `action: execute-action` - generated when a rule schedules an action to run + - `action: new-instance` - generated when a rule has a new instance id that is active + - `action: recovered-instance` - generated when a rule has a previously active instance id that is no longer active + - `action: active-instance` - generated when a rule determines an instance id is active For the `saved_objects` array elements, these are references to saved objects -associated with the event. For the `alerting` provider, those are alert saved -ojects and for the `actions` provider those are action saved objects. The -`alerts:execute-action` event includes both the alert and action saved object -references. For that event, only the alert reference has the optional `rel` +associated with the event. For the `alerting` provider, those are rule saved +ojects and for the `actions` provider those are connector saved objects. The +`alerts:execute-action` event includes both the rule and connector saved object +references. For that event, only the rule reference has the optional `rel` property with a `primary` value. This property is used when searching the event log to indicate which saved objects should be directly searchable via -saved object references. For the `alerts:execute-action` event, searching -only via the alert saved object reference will return the event. +saved object references. For the `alerts:execute-action` event, only searching +via the rule saved object reference will return the event; searching via the +connector save object reference will **NOT** return the event. The +`actions:execute` event also includes both the rule and connector saved object +references, and both of them have the `rel` property with a `primary` value, +allowing those events to be returned in searches of either the rule or +connector. ## Event Log index - associated resources From 089a175a5b6447dfaa6658a6182f2b164ac0f9d4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:29:20 -0400 Subject: [PATCH 081/118] [App Search] Convert Schema pages to new page template (#102846) (#102996) * Convert Schema page to new page template + update empty state - remove panel wrapper, add create schema field modal * Convert ReindexJob view to new page template + remove breadcrumb prop * Convert Meta Engine Schema view to new page template * Update routers * [Polish] Misc Davey Schema UI tweaks - see https://github.com/elastic/kibana/pull/101958/files + change color away from secondary, since that's going away in EUI at some point * [UX] Fix SchemaAddFieldModal stuttering on first new schema field add - With the new template, transitioning from the empty state to the filled schema state causes the modal to stutter due to the component rerender - Changing the page to not instantly react/update `hasSchema` when local schema state changes but instead to wait for the server call to finish and for cachedSchema to update fixes the UX problem * [UI polish] Revert button color change per Davey's feedback Co-authored-by: Constance --- .../components/engine/engine_router.tsx | 10 +- .../schema/components/empty_state.test.tsx | 11 ++ .../schema/components/empty_state.tsx | 16 ++- .../schema/reindex_job/reindex_job.test.tsx | 17 +-- .../schema/reindex_job/reindex_job.tsx | 61 ++++---- .../components/schema/schema_logic.test.ts | 8 +- .../components/schema/schema_logic.ts | 5 +- .../components/schema/schema_router.tsx | 12 +- .../schema/views/meta_engine_schema.test.tsx | 10 +- .../schema/views/meta_engine_schema.tsx | 131 +++++++++--------- .../components/schema/views/schema.test.tsx | 28 +--- .../components/schema/views/schema.tsx | 50 +++---- .../shared/schema/add_field_modal/index.tsx | 10 +- 13 files changed, 172 insertions(+), 197 deletions(-) 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 fc057858426d2..91a21847107a9 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 @@ -109,6 +109,11 @@ export const EngineRouter: React.FC = () => { )} + {canViewEngineSchema && ( + + + + )} {canManageEngineSearchUi && ( @@ -121,11 +126,6 @@ export const EngineRouter: React.FC = () => { )} {/* TODO: Remove layout once page template migration is over */} }> - {canViewEngineSchema && ( - - - - )} {canManageEngineCurations && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx index ea658c741b8a0..1b353f17855d2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx @@ -5,12 +5,16 @@ * 2.0. */ +import { setMockValues } from '../../../../__mocks__/kea_logic'; + import React from 'react'; import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { SchemaAddFieldModal } from '../../../../shared/schema'; + import { EmptyState } from './'; describe('EmptyState', () => { @@ -24,4 +28,11 @@ describe('EmptyState', () => { expect.stringContaining('#indexing-documents-guide-schema') ); }); + + it('renders a modal that lets a user add a new schema field', () => { + setMockValues({ isModalOpen: true }); + const wrapper = shallow(); + + expect(wrapper.find(SchemaAddFieldModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx index 6d7dd198d5eef..ad9285c7b8fef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx @@ -7,14 +7,21 @@ import React from 'react'; -import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { useValues, useActions } from 'kea'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { SchemaAddFieldModal } from '../../../../shared/schema'; import { DOCS_PREFIX } from '../../../routes'; +import { SchemaLogic } from '../schema_logic'; export const EmptyState: React.FC = () => { + const { isModalOpen } = useValues(SchemaLogic); + const { addSchemaField, closeModal } = useActions(SchemaLogic); + return ( - + <> { } /> - + {isModalOpen && ( + + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx index e76ab60005231..4dd7a869ca27e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx @@ -14,15 +14,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Loading } from '../../../../shared/loading'; import { SchemaErrorsAccordion } from '../../../../shared/schema'; import { ReindexJob } from './'; describe('ReindexJob', () => { - const props = { - schemaBreadcrumb: ['Engines', 'some-engine', 'Schema'], - }; const values = { dataLoading: false, fieldCoercionErrors: {}, @@ -43,27 +39,20 @@ describe('ReindexJob', () => { }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(SchemaErrorsAccordion)).toHaveLength(1); expect(wrapper.find(SchemaErrorsAccordion).prop('generateViewPath')).toHaveLength(1); }); it('calls loadReindexJob on page load', () => { - shallow(); + shallow(); expect(actions.loadReindexJob).toHaveBeenCalledWith('abc1234567890'); }); - it('renders a loading state', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders schema errors with links to document pages', () => { - const wrapper = shallow(); + const wrapper = shallow(); const generateViewPath = wrapper .find(SchemaErrorsAccordion) .prop('generateViewPath') as Function; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx index 576b4ae11603b..b0a8cbd25f8b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx @@ -10,25 +10,17 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; -import { Loading } from '../../../../shared/loading'; import { SchemaErrorsAccordion } from '../../../../shared/schema'; - import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../routes'; -import { EngineLogic, generateEnginePath } from '../../engine'; +import { EngineLogic, generateEnginePath, getEngineBreadcrumbs } from '../../engine'; +import { AppSearchPageTemplate } from '../../layout'; +import { SCHEMA_TITLE } from '../constants'; import { ReindexJobLogic } from './reindex_job_logic'; -interface Props { - schemaBreadcrumb: BreadcrumbTrail; -} - -export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => { +export const ReindexJob: React.FC = () => { const { reindexJobId } = useParams() as { reindexJobId: string }; const { loadReindexJob } = useActions(ReindexJobLogic); const { dataLoading, fieldCoercionErrors } = useValues(ReindexJobLogic); @@ -40,34 +32,29 @@ export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => { loadReindexJob(reindexJobId); }, [reindexJobId]); - if (dataLoading) return ; - return ( - <> - - + + generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { documentId }) + } /> - - - - generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { documentId }) - } - /> - - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts index 7687296cf9f83..dcc5747b0d32f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts @@ -140,13 +140,13 @@ describe('SchemaLogic', () => { describe('selectors', () => { describe('hasSchema', () => { - it('returns true when the schema obj has items', () => { - mountAndSetSchema({ schema: { test: SchemaType.Text } }); + it('returns true when the cached server schema obj has items', () => { + mount({ cachedSchema: { test: SchemaType.Text } }); expect(SchemaLogic.values.hasSchema).toEqual(true); }); - it('returns false when the schema obj is empty', () => { - mountAndSetSchema({ schema: {} }); + it('returns false when the cached server schema obj is empty', () => { + mount({ schema: {} }); expect(SchemaLogic.values.hasSchema).toEqual(false); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts index 3215a46c8e299..3dcafd6782afd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts @@ -108,7 +108,10 @@ export const SchemaLogic = kea>({ ], }, selectors: { - hasSchema: [(selectors) => [selectors.schema], (schema) => Object.keys(schema).length > 0], + hasSchema: [ + (selectors) => [selectors.cachedSchema], + (cachedSchema) => Object.keys(cachedSchema).length > 0, + ], hasSchemaChanged: [ (selectors) => [selectors.schema, selectors.cachedSchema], (schema, cachedSchema) => !isEqual(schema, cachedSchema), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx index bfa346fee468b..d358c489593c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx @@ -10,27 +10,21 @@ import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { ENGINE_REINDEX_JOB_PATH } from '../../routes'; -import { EngineLogic, getEngineBreadcrumbs } from '../engine'; +import { EngineLogic } from '../engine'; -import { SCHEMA_TITLE } from './constants'; import { ReindexJob } from './reindex_job'; import { Schema, MetaEngineSchema } from './views'; export const SchemaRouter: React.FC = () => { const { isMetaEngine } = useValues(EngineLogic); - const schemaBreadcrumb = getEngineBreadcrumbs([SCHEMA_TITLE]); return ( - - - - - {isMetaEngine ? : } + + {isMetaEngine ? : } ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx index 1d677ad08db43..60a0513b774fd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx @@ -7,6 +7,7 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic'; import '../../../../__mocks__/shallow_useeffect.mock'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -14,8 +15,6 @@ import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; -import { Loading } from '../../../../shared/loading'; - import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; import { MetaEngineSchema } from './'; @@ -46,13 +45,6 @@ describe('MetaEngineSchema', () => { expect(actions.loadSchema).toHaveBeenCalled(); }); - it('renders a loading state', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders an inactive fields callout & table when source engines have schema conflicts', () => { setMockValues({ ...values, hasConflicts: true, conflictingFieldsCount: 5 }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx index 4c0235cf81129..2eb8bac00a040 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx @@ -9,14 +9,15 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiPageContentBody, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../../shared/flash_messages'; -import { Loading } from '../../../../shared/loading'; import { DataPanel } from '../../data_panel'; +import { getEngineBreadcrumbs } from '../../engine'; +import { AppSearchPageTemplate } from '../../layout'; import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; +import { SCHEMA_TITLE } from '../constants'; import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; export const MetaEngineSchema: React.FC = () => { @@ -27,90 +28,88 @@ export const MetaEngineSchema: React.FC = () => { loadSchema(); }, []); - if (dataLoading) return ; - return ( - <> - - - - {hasConflicts && ( - <> - + {hasConflicts && ( + <> + +

    + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription', { defaultMessage: - '{conflictingFieldsCount, plural, one {# field is} other {# fields are}} not searchable', - values: { conflictingFieldsCount }, + 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.', } )} - > -

    - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription', - { - defaultMessage: - 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.', - } - )} -

    -
    - - +

    +
    + + + )} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle', + { defaultMessage: 'Active fields' } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription', + { defaultMessage: 'Fields which belong to one or more engine.' } )} + > + + + + {hasConflicts && ( {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle', - { defaultMessage: 'Active fields' } + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle', + { defaultMessage: 'Inactive fields' } )} } subtitle={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription', - { defaultMessage: 'Fields which belong to one or more engine.' } + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription', + { + defaultMessage: + 'These fields have type conflicts. To activate these fields, change types in the source engines to match.', + } )} > - + - - {hasConflicts && ( - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle', - { defaultMessage: 'Inactive fields' } - )} - - } - subtitle={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription', - { - defaultMessage: - 'These fields have type conflicts. To activate these fields, change types in the source engines to match.', - } - )} - > - - - )} -
    - + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx index 91ec8eda55fc3..cae16d70592fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx @@ -7,17 +7,18 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic'; import '../../../../__mocks__/shallow_useeffect.mock'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader, EuiButton } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; -import { Loading } from '../../../../shared/loading'; import { SchemaAddFieldModal } from '../../../../shared/schema'; +import { getPageHeaderActions } from '../../../../test_helpers'; -import { SchemaCallouts, SchemaTable, EmptyState } from '../components'; +import { SchemaCallouts, SchemaTable } from '../components'; import { Schema } from './'; @@ -56,27 +57,8 @@ describe('Schema', () => { expect(actions.loadSchema).toHaveBeenCalled(); }); - it('renders a loading state', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - - it('renders an empty state', () => { - setMockValues({ ...values, hasSchema: false }); - const wrapper = shallow(); - - expect(wrapper.find(EmptyState)).toHaveLength(1); - }); - describe('page action buttons', () => { - const subject = () => - shallow() - .find(EuiPageHeader) - .dive() - .children() - .dive(); + const subject = () => getPageHeaderActions(shallow()); it('renders', () => { const wrapper = subject(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx index 7bc995b16468a..d2a760e8accff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx @@ -9,14 +9,15 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiButton, EuiPageContentBody } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../../shared/flash_messages'; -import { Loading } from '../../../../shared/loading'; import { SchemaAddFieldModal } from '../../../../shared/schema'; +import { getEngineBreadcrumbs } from '../../engine'; +import { AppSearchPageTemplate } from '../../layout'; import { SchemaCallouts, SchemaTable, EmptyState } from '../components'; +import { SCHEMA_TITLE } from '../constants'; import { SchemaLogic } from '../schema_logic'; export const Schema: React.FC = () => { @@ -31,19 +32,18 @@ export const Schema: React.FC = () => { loadSchema(); }, []); - if (dataLoading) return ; - return ( - <> - { > {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.schema.updateSchemaButtonLabel', - { defaultMessage: 'Update types' } + { defaultMessage: 'Save changes' } )} , { { defaultMessage: 'Create a schema field' } )} , - ]} - /> - - - - {hasSchema ? : } - {isModalOpen && ( - - )} - - + ], + }} + isLoading={dataLoading} + isEmptyState={!hasSchema} + emptyState={} + > + + + {isModalOpen && ( + + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx index 902417d02665e..ba9da900c0145 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx @@ -10,6 +10,7 @@ import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'; import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -83,8 +84,13 @@ export const SchemaAddFieldModal: React.FC = ({ {ADD_FIELD_MODAL_TITLE} -

    {ADD_FIELD_MODAL_DESCRIPTION}

    - + {ADD_FIELD_MODAL_DESCRIPTION}

    } + /> + From 0a44a53da7971a02ef20be2b6c1a971a90c058eb Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 22 Jun 2021 18:03:15 -0400 Subject: [PATCH 082/118] [Alerting] Add event log entry when an action starts executing (#102370) (#103001) * First steps for adding action execution to event log * Fix tests * Move the event to the actions plugin * Update functional tests * Fix tests * Fix types --- .../actions/server/constants/event_log.ts | 1 + .../server/lib/action_executor.test.ts | 47 ++++++++++- .../actions/server/lib/action_executor.ts | 12 +++ .../tests/actions/execute.ts | 78 +++++++++++++++---- .../spaces_only/tests/actions/execute.ts | 50 ++++++++---- 5 files changed, 156 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/actions/server/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts index 508709c8783ab..9163a0d105ce8 100644 --- a/x-pack/plugins/actions/server/constants/event_log.ts +++ b/x-pack/plugins/actions/server/constants/event_log.ts @@ -8,5 +8,6 @@ export const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { execute: 'execute', + executeStart: 'execute-start', executeViaHttp: 'execute-via-http', }; diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 8ec94c4d4a552..37d461d6b2a50 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -23,6 +23,7 @@ const services = actionsMock.createServices(); const actionsClient = actionsClientMock.create(); const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const actionTypeRegistry = actionTypeRegistryMock.create(); +const eventLogger = eventLoggerMock.create(); const executeParams = { actionId: '1', @@ -42,7 +43,7 @@ actionExecutor.initialize({ getActionsClientWithRequest, actionTypeRegistry, encryptedSavedObjectsClient, - eventLogger: eventLoggerMock.create(), + eventLogger, preconfiguredActions: [], }); @@ -379,6 +380,50 @@ test('logs a warning when alert executor returns invalid status', async () => { ); }); +test('writes to event log for execute and execute start', async () => { + const executorMock = setupActionExecutorMock(); + executorMock.mockResolvedValue({ + actionId: '1', + status: 'ok', + }); + await actionExecutor.execute(executeParams); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); + expect(eventLogger.logEvent.mock.calls[0][0]).toMatchObject({ + event: { + action: 'execute-start', + }, + kibana: { + saved_objects: [ + { + rel: 'primary', + type: 'action', + id: '1', + type_id: 'test', + namespace: 'some-namespace', + }, + ], + }, + message: 'action started: test:1: action-1', + }); + expect(eventLogger.logEvent.mock.calls[1][0]).toMatchObject({ + event: { + action: 'execute', + }, + kibana: { + saved_objects: [ + { + rel: 'primary', + type: 'action', + id: '1', + type_id: 'test', + namespace: 'some-namespace', + }, + ], + }, + message: 'action executed: test:1: action-1', + }); +}); + function setupActionExecutorMock() { const actionType: jest.Mocked = { id: 'test', diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 9d2b937734fb0..e9e7b17288611 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -7,6 +7,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { Logger, KibanaRequest } from 'src/core/server'; +import { cloneDeep } from 'lodash'; import { withSpan } from '@kbn/apm-utils'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { @@ -168,6 +169,17 @@ export class ActionExecutor { } eventLogger.startTiming(event); + + const startEvent = cloneDeep({ + ...event, + event: { + ...event.event, + action: EVENT_LOG_ACTIONS.executeStart, + }, + message: `action started: ${actionLabel}`, + }); + eventLogger.logEvent(startEvent); + let rawResult: ActionTypeExecutorResult; try { rawResult = await actionType.executor({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index f7d7c1df8fd46..5c578d2d08dae 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -519,47 +519,93 @@ export default function ({ getService }: FtrProviderContext) { type: 'action', id: connectorId, provider: 'actions', - actions: new Map([['execute', { equal: 1 }]]), - filter: 'event.action:(execute)', + actions: new Map([ + ['execute-start', { equal: 1 }], + ['execute', { equal: 1 }], + ]), + // filter: 'event.action:(execute)', }); }); - const event = events[0]; + const startExecuteEvent = events[0]; + const executeEvent = events[1]; - const duration = event?.event?.duration; - const eventStart = Date.parse(event?.event?.start || 'undefined'); - const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const duration = executeEvent?.event?.duration; + const executeEventStart = Date.parse(executeEvent?.event?.start || 'undefined'); + const startExecuteEventStart = Date.parse(startExecuteEvent?.event?.start || 'undefined'); + const executeEventEnd = Date.parse(executeEvent?.event?.end || 'undefined'); const dateNow = Date.now(); expect(typeof duration).to.be('number'); - expect(eventStart).to.be.ok(); - expect(eventEnd).to.be.ok(); + expect(executeEventStart).to.be.ok(); + expect(startExecuteEventStart).to.equal(executeEventStart); + expect(executeEventEnd).to.be.ok(); const durationDiff = Math.abs( - Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + Math.round(duration! / NANOS_IN_MILLIS) - (executeEventEnd - executeEventStart) ); // account for rounding errors expect(durationDiff < 1).to.equal(true); - expect(eventStart <= eventEnd).to.equal(true); - expect(eventEnd <= dateNow).to.equal(true); + expect(executeEventStart <= executeEventEnd).to.equal(true); + expect(executeEventEnd <= dateNow).to.equal(true); - expect(event?.event?.outcome).to.equal(outcome); + expect(executeEvent?.event?.outcome).to.equal(outcome); - expect(event?.kibana?.saved_objects).to.eql([ + expect(executeEvent?.kibana?.saved_objects).to.eql([ { rel: 'primary', type: 'action', id: connectorId, + namespace: 'space1', type_id: actionTypeId, - namespace: spaceId, }, ]); + expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects); - expect(event?.message).to.eql(message); + expect(executeEvent?.message).to.eql(message); + expect(startExecuteEvent?.message).to.eql(message.replace('executed', 'started')); if (errorMessage) { - expect(event?.error?.message).to.eql(errorMessage); + expect(executeEvent?.error?.message).to.eql(errorMessage); } + + // const event = events[0]; + + // const duration = event?.event?.duration; + // const eventStart = Date.parse(event?.event?.start || 'undefined'); + // const eventEnd = Date.parse(event?.event?.end || 'undefined'); + // const dateNow = Date.now(); + + // expect(typeof duration).to.be('number'); + // expect(eventStart).to.be.ok(); + // expect(eventEnd).to.be.ok(); + + // const durationDiff = Math.abs( + // Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + // ); + + // // account for rounding errors + // expect(durationDiff < 1).to.equal(true); + // expect(eventStart <= eventEnd).to.equal(true); + // expect(eventEnd <= dateNow).to.equal(true); + + // expect(event?.event?.outcome).to.equal(outcome); + + // expect(event?.kibana?.saved_objects).to.eql([ + // { + // rel: 'primary', + // type: 'action', + // id: connectorId, + // type_id: actionTypeId, + // namespace: spaceId, + // }, + // ]); + + // expect(event?.message).to.eql(message); + + // if (errorMessage) { + // expect(event?.error?.message).to.eql(errorMessage); + // } } } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 147b6abfb88d1..d494c99c80e8f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -100,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) { actionTypeId: 'test.index-record', outcome: 'success', message: `action executed: test.index-record:${createdAction.id}: My action`, + startMessage: `action started: test.index-record:${createdAction.id}: My action`, }); }); @@ -336,10 +337,19 @@ export default function ({ getService }: FtrProviderContext) { outcome: string; message: string; errorMessage?: string; + startMessage?: string; } async function validateEventLog(params: ValidateEventLogParams): Promise { - const { spaceId, actionId, actionTypeId, outcome, message, errorMessage } = params; + const { + spaceId, + actionId, + actionTypeId, + outcome, + message, + startMessage, + errorMessage, + } = params; const events: IValidatedEvent[] = await retry.try(async () => { return await getEventLog({ @@ -348,33 +358,39 @@ export default function ({ getService }: FtrProviderContext) { type: 'action', id: actionId, provider: 'actions', - actions: new Map([['execute', { equal: 1 }]]), + actions: new Map([ + ['execute-start', { equal: 1 }], + ['execute', { equal: 1 }], + ]), }); }); - const event = events[0]; + const startExecuteEvent = events[0]; + const executeEvent = events[1]; - const duration = event?.event?.duration; - const eventStart = Date.parse(event?.event?.start || 'undefined'); - const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const duration = executeEvent?.event?.duration; + const executeEventStart = Date.parse(executeEvent?.event?.start || 'undefined'); + const startExecuteEventStart = Date.parse(startExecuteEvent?.event?.start || 'undefined'); + const executeEventEnd = Date.parse(executeEvent?.event?.end || 'undefined'); const dateNow = Date.now(); expect(typeof duration).to.be('number'); - expect(eventStart).to.be.ok(); - expect(eventEnd).to.be.ok(); + expect(executeEventStart).to.be.ok(); + expect(startExecuteEventStart).to.equal(executeEventStart); + expect(executeEventEnd).to.be.ok(); const durationDiff = Math.abs( - Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + Math.round(duration! / NANOS_IN_MILLIS) - (executeEventEnd - executeEventStart) ); // account for rounding errors expect(durationDiff < 1).to.equal(true); - expect(eventStart <= eventEnd).to.equal(true); - expect(eventEnd <= dateNow).to.equal(true); + expect(executeEventStart <= executeEventEnd).to.equal(true); + expect(executeEventEnd <= dateNow).to.equal(true); - expect(event?.event?.outcome).to.equal(outcome); + expect(executeEvent?.event?.outcome).to.equal(outcome); - expect(event?.kibana?.saved_objects).to.eql([ + expect(executeEvent?.kibana?.saved_objects).to.eql([ { rel: 'primary', type: 'action', @@ -383,11 +399,15 @@ export default function ({ getService }: FtrProviderContext) { type_id: actionTypeId, }, ]); + expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects); - expect(event?.message).to.eql(message); + expect(executeEvent?.message).to.eql(message); + if (startMessage) { + expect(startExecuteEvent?.message).to.eql(startMessage); + } if (errorMessage) { - expect(event?.error?.message).to.eql(errorMessage); + expect(executeEvent?.error?.message).to.eql(errorMessage); } } } From 51ce75a47f24e8a9522ada6c819b346f6ff69ee6 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 22 Jun 2021 19:02:27 -0400 Subject: [PATCH 083/118] [ML] Anomaly Detection: Visualize delayed - data Part 2 (#102270) (#103011) * add link in datafeed tab.remove interval * add annotation overlay to chart * adds annotations checkbox * ensure annotation with same start/end time show up in chart * update annotations time format * move time format to client * adds info tooltip to modal title * adds model snapshots to datafeed chart --- x-pack/plugins/ml/common/types/results.ts | 4 + .../annotations_table/annotations_table.js | 10 +- .../components/datafeed_modal/constants.ts | 2 +- .../datafeed_modal/datafeed_modal.tsx | 213 ++++++++++++++---- .../datafeed_modal/get_interval_options.ts | 118 ---------- .../components/job_details/job_details.js | 76 +++++-- .../job_details/job_details_pane.js | 13 +- .../services/ml_api_service/results.ts | 7 +- .../models/results_service/results_service.ts | 66 +++++- 9 files changed, 308 insertions(+), 201 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts index fa40cefcaed48..74d3286438588 100644 --- a/x-pack/plugins/ml/common/types/results.ts +++ b/x-pack/plugins/ml/common/types/results.ts @@ -6,6 +6,7 @@ */ import { estypes } from '@elastic/elasticsearch'; +import { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts'; export interface GetStoppedPartitionResult { jobs: string[] | Record; @@ -13,6 +14,9 @@ export interface GetStoppedPartitionResult { export interface GetDatafeedResultsChartDataResult { bucketResults: number[][]; datafeedResults: number[][]; + annotationResultsRect: RectAnnotationDatum[]; + annotationResultsLine: LineAnnotationDatum[]; + modelSnapshotResultsLine: LineAnnotationDatum[]; } export interface DatafeedResultsChartDataParams { diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index afed7e79ff757..b68e64a5d9f6a 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -494,13 +494,13 @@ class AnnotationsTableUI extends Component { render: (annotation) => { const viewDataFeedText = ( ); const viewDataFeedTooltipAriaLabelText = i18n.translate( - 'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel', - { defaultMessage: 'View datafeed' } + 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel', + { defaultMessage: 'Datafeed chart' } ); return ( ) : null} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts index 71f3795518bc9..b3b9487523196 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts @@ -15,7 +15,7 @@ export const CHART_DIRECTION = { export type ChartDirectionType = typeof CHART_DIRECTION[keyof typeof CHART_DIRECTION]; // [width, height] -export const CHART_SIZE: ChartSizeArray = ['100%', 300]; +export const CHART_SIZE: ChartSizeArray = ['100%', 380]; export const TAB_IDS = { CHART: 'chart', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx index cf547a49cac4c..2dece82e6f5c7 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx @@ -11,25 +11,35 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { EuiButtonEmpty, + EuiCheckbox, EuiDatePicker, EuiFlexGroup, EuiFlexItem, + EuiIcon, + EuiIconTip, EuiLoadingChart, EuiModal, EuiModalHeader, EuiModalBody, - EuiSelect, EuiSpacer, EuiTabs, EuiTab, + EuiText, + EuiTitle, EuiToolTip, + htmlIdGenerator, } from '@elastic/eui'; import { + AnnotationDomainType, Axis, Chart, CurveType, + LineAnnotation, LineSeries, + LineAnnotationDatum, Position, + RectAnnotation, + RectAnnotationDatum, ScaleType, Settings, timeFormatter, @@ -42,7 +52,6 @@ import { useMlApiContext } from '../../../../contexts/kibana'; import { useCurrentEuiTheme } from '../../../../components/color_range_legend'; import { JobMessagesPane } from '../job_details/job_messages_pane'; import { EditQueryDelay } from './edit_query_delay'; -import { getIntervalOptions } from './get_interval_options'; import { CHART_DIRECTION, ChartDirectionType, @@ -53,12 +62,18 @@ import { } from './constants'; import { loadFullJob } from '../utils'; -const dateFormatter = timeFormatter('MM-DD HH:mm'); +const dateFormatter = timeFormatter('MM-DD HH:mm:ss'); +const MAX_CHART_POINTS = 480; interface DatafeedModalProps { jobId: string; end: number; - onClose: (deletionApproved?: boolean) => void; + onClose: () => void; +} + +function setLineAnnotationHeader(lineDatum: LineAnnotationDatum) { + lineDatum.header = dateFormatter(lineDatum.dataValue); + return lineDatum; } export const DatafeedModal: FC = ({ jobId, end, onClose }) => { @@ -68,11 +83,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) = isInitialized: boolean; }>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false }); const [endDate, setEndDate] = useState(moment(end)); - const [interval, setInterval] = useState(); const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.CHART); const [isLoadingChartData, setIsLoadingChartData] = useState(false); const [bucketData, setBucketData] = useState([]); + const [annotationData, setAnnotationData] = useState<{ + rect: RectAnnotationDatum[]; + line: LineAnnotationDatum[]; + }>({ rect: [], line: [] }); + const [modelSnapshotData, setModelSnapshotData] = useState([]); const [sourceData, setSourceData] = useState([]); + const [showAnnotations, setShowAnnotations] = useState(true); + const [showModelSnapshots, setShowModelSnapshots] = useState(true); const { results: { getDatafeedResultChartData }, @@ -102,25 +123,30 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) = const handleChange = (date: moment.Moment) => setEndDate(date); const handleEndDateChange = (direction: ChartDirectionType) => { - if (interval === undefined) return; + if (data.bucketSpan === undefined) return; const newEndDate = endDate.clone(); - const [count, type] = interval.split(' '); + const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!; + const unit = unitMatch[0]; + const count = Number(data.bucketSpan.replace(/[^0-9]/g, '')); if (direction === CHART_DIRECTION.FORWARD) { - newEndDate.add(Number(count), type); + newEndDate.add(MAX_CHART_POINTS * count, unit); } else { - newEndDate.subtract(Number(count), type); + newEndDate.subtract(MAX_CHART_POINTS * count, unit); } setEndDate(newEndDate); }; const getChartData = useCallback(async () => { - if (interval === undefined) return; + if (data.bucketSpan === undefined) return; const endTimestamp = moment(endDate).valueOf(); - const [count, type] = interval.split(' '); - const startMoment = endDate.clone().subtract(Number(count), type); + const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!; + const unit = unitMatch[0]; + const count = Number(data.bucketSpan.replace(/[^0-9]/g, '')); + // STARTTIME = ENDTIME - (BucketSpan * MAX_CHART_POINTS) + const startMoment = endDate.clone().subtract(MAX_CHART_POINTS * count, unit); const startTimestamp = moment(startMoment).valueOf(); try { @@ -128,6 +154,11 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) = setSourceData(chartData.datafeedResults); setBucketData(chartData.bucketResults); + setAnnotationData({ + rect: chartData.annotationResultsRect, + line: chartData.annotationResultsLine.map(setLineAnnotationHeader), + }); + setModelSnapshotData(chartData.modelSnapshotResultsLine.map(setLineAnnotationHeader)); } catch (error) { const title = i18n.translate('xpack.ml.jobsList.datafeedModal.errorToastTitle', { defaultMessage: 'Error fetching data', @@ -135,7 +166,7 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) = displayErrorToast(error, title); } setIsLoadingChartData(false); - }, [endDate, interval]); + }, [endDate, data.bucketSpan]); const getJobData = async () => { try { @@ -145,11 +176,6 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) = bucketSpan: job.analysis_config.bucket_span, isInitialized: true, }); - const intervalOptions = getIntervalOptions(job.analysis_config.bucket_span); - const initialInterval = intervalOptions.length - ? intervalOptions[intervalOptions.length - 1] - : undefined; - setInterval(initialInterval?.value || '72 hours'); } catch (error) { displayErrorToast(error); } @@ -161,20 +187,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) = useEffect( function loadChartData() { - if (interval !== undefined) { + if (data.bucketSpan !== undefined) { setIsLoadingChartData(true); getChartData(); } }, - [endDate, interval] + [endDate, data.bucketSpan] ); const { datafeedConfig, bucketSpan, isInitialized } = data; - - const intervalOptions = useMemo(() => { - if (bucketSpan === undefined) return []; - return getIntervalOptions(bucketSpan); - }, [bucketSpan]); + const checkboxIdAnnotation = useMemo(() => htmlIdGenerator()(), []); + const checkboxIdModelSnapshot = useMemo(() => htmlIdGenerator()(), []); return ( = ({ jobId, end, onClose }) = - + + + + } + /> + + + +

    + +

    +
    +
    +
    = ({ jobId, end, onClose }) = - - setInterval(e.target.value)} - aria-label={i18n.translate( - 'xpack.ml.jobsList.datafeedModal.intervalSelection', - { - defaultMessage: 'Datafeed modal chart interval selection', - } - )} - /> - = ({ jobId, end, onClose }) = isEnabled={datafeedConfig.state === DATAFEED_STATE.STOPPED} /> + + + + + + + } + checked={showAnnotations} + onChange={() => setShowAnnotations(!showAnnotations)} + /> + + + + + + } + checked={showModelSnapshots} + onChange={() => setShowModelSnapshots(!showModelSnapshots)} + /> + + + @@ -298,7 +362,65 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) = })} position={Position.Left} /> + {showModelSnapshots ? ( + } + markerPosition={Position.Top} + style={{ + line: { + strokeWidth: 3, + stroke: euiTheme.euiColorVis1, + opacity: 0.5, + }, + }} + /> + ) : null} + {showAnnotations ? ( + <> + } + markerPosition={Position.Top} + style={{ + line: { + strokeWidth: 3, + stroke: euiTheme.euiColorDangerText, + opacity: 0.5, + }, + }} + /> + + + ) : null} = ({ jobId, end, onClose }) = curve={CurveType.LINEAR} /> { - const unitMatch = bucketSpan.match(/[d | h| m | s]/g)!; - const unit = unitMatch[0]; - const count = Number(bucketSpan.replace(/[^0-9]/g, '')); - - const intervalOptions = []; - - if (['s', 'ms', 'micros', 'nanos'].includes(unit)) { - intervalOptions.push( - { - value: '1 hour', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.1hourOption', { - defaultMessage: '{count} hour', - values: { count: 1 }, - }), - }, - { - value: '2 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.2hourOption', { - defaultMessage: '{count} hours', - values: { count: 2 }, - }), - } - ); - } - - if ((unit === 'm' && count <= 4) || unit === 'h') { - intervalOptions.push( - { - value: '3 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.3hourOption', { - defaultMessage: '{count} hours', - values: { count: 3 }, - }), - }, - { - value: '8 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.8hourOption', { - defaultMessage: '{count} hours', - values: { count: 8 }, - }), - }, - { - value: '12 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.12hourOption', { - defaultMessage: '{count} hours', - values: { count: 12 }, - }), - }, - { - value: '24 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.24hourOption', { - defaultMessage: '{count} hours', - values: { count: 24 }, - }), - } - ); - } - - if ((unit === 'm' && count >= 5 && count <= 15) || unit === 'h') { - intervalOptions.push( - { - value: '48 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.48hourOption', { - defaultMessage: '{count} hours', - values: { count: 48 }, - }), - }, - { - value: '72 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.72hourOption', { - defaultMessage: '{count} hours', - values: { count: 72 }, - }), - } - ); - } - - if ((unit === 'm' && count >= 10 && count <= 15) || unit === 'h' || unit === 'd') { - intervalOptions.push( - { - value: '5 days', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.5daysOption', { - defaultMessage: '{count} days', - values: { count: 5 }, - }), - }, - { - value: '7 days', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.7daysOption', { - defaultMessage: '{count} days', - values: { count: 7 }, - }), - } - ); - } - - if (unit === 'h' || unit === 'd') { - intervalOptions.push({ - value: '14 days', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.14DaysOption', { - defaultMessage: '{count} days', - values: { count: 14 }, - }), - }); - } - - return intervalOptions; -}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index b514c8433daf4..d3856e6afa398 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -7,26 +7,29 @@ import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; - -import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; import { extractJobDetails } from './extract_job_details'; import { JsonPane } from './json_tab'; import { DatafeedPreviewPane } from './datafeed_preview_tab'; import { AnnotationsTable } from '../../../../components/annotations/annotations_table'; +import { DatafeedModal } from '../datafeed_modal'; import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout'; import { ModelSnapshotTable } from '../../../../components/model_snapshots'; import { ForecastsTable } from './forecasts_table'; import { JobDetailsPane } from './job_details_pane'; import { JobMessagesPane } from './job_messages_pane'; -import { i18n } from '@kbn/i18n'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; export class JobDetailsUI extends Component { constructor(props) { super(props); - this.state = {}; + this.state = { + datafeedModalVisible: false, + }; if (this.props.addYourself) { this.props.addYourself(props.jobId, (j) => this.updateJob(j)); } @@ -77,6 +80,30 @@ export class JobDetailsUI extends Component { alertRules, } = extractJobDetails(job, basePath, refreshJobList); + datafeed.titleAction = ( + + } + > + + this.setState({ + datafeedModalVisible: true, + }) + } + /> + + ); + const tabs = [ { id: 'job-settings', @@ -105,6 +132,32 @@ export class JobDetailsUI extends Component { /> ), }, + { + id: 'datafeed', + 'data-test-subj': 'mlJobListTab-datafeed', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', { + defaultMessage: 'Datafeed', + }), + content: ( + <> + + {this.props.jobId && this.state.datafeedModalVisible ? ( + { + this.setState({ + datafeedModalVisible: false, + }); + }} + end={job.data_counts.latest_bucket_timestamp} + jobId={this.props.jobId} + /> + ) : null} + + ), + }, { id: 'counts', 'data-test-subj': 'mlJobListTab-counts', @@ -137,21 +190,6 @@ export class JobDetailsUI extends Component { ]; if (showFullDetails && datafeed.items.length) { - // Datafeed should be at index 2 in tabs array for full details - tabs.splice(2, 0, { - id: 'datafeed', - 'data-test-subj': 'mlJobListTab-datafeed', - name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', { - defaultMessage: 'Datafeed', - }), - content: ( - - ), - }); - tabs.push( { id: 'datafeed-preview', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js index 49d9bcde49052..4046f4d5d8071 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js @@ -9,6 +9,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { + EuiFlexGroup, + EuiFlexItem, EuiTitle, EuiTable, EuiTableBody, @@ -42,9 +44,14 @@ function Section({ section }) { return ( - -

    {section.title}

    -
    + + + +

    {section.title}

    +
    +
    + {section.titleAction} +
    diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts index 19ba5aa304bf0..25ef36782207f 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts @@ -6,7 +6,10 @@ */ // Service for obtaining data for the ML Results dashboards. -import { GetStoppedPartitionResult } from '../../../../common/types/results'; +import { + GetStoppedPartitionResult, + GetDatafeedResultsChartDataResult, +} from '../../../../common/types/results'; import { HttpService } from '../http_service'; import { basePath } from './index'; import { JobId } from '../../../../common/types/anomaly_detection_jobs'; @@ -148,7 +151,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({ start, end, }); - return httpService.http({ + return httpService.http({ path: `${basePath()}/results/datafeed_results_chart`, method: 'POST', body, diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 9413ee00184d2..81ee394b99704 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -27,6 +27,7 @@ import { import { MlJobsResponse } from '../../../common/types/job_service'; import type { MlClient } from '../../lib/ml_client'; import { datafeedsProvider } from '../job_service/datafeeds'; +import { annotationServiceProvider } from '../annotation_service'; // Service for carrying out Elasticsearch queries to obtain data for the // ML Results dashboards. @@ -620,13 +621,19 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust const finalResults: GetDatafeedResultsChartDataResult = { bucketResults: [], datafeedResults: [], + annotationResultsRect: [], + annotationResultsLine: [], + modelSnapshotResultsLine: [], }; const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient); - const datafeedConfig = await getDatafeedByJobId(jobId); - const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId }); - if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) { + const [datafeedConfig, { body: jobsResponse }] = await Promise.all([ + getDatafeedByJobId(jobId), + mlClient.getJobs({ job_id: jobId }), + ]); + + if (jobsResponse && (jobsResponse.count === 0 || jobsResponse.jobs === undefined)) { throw Boom.notFound(`Job with the id "${jobId}" not found`); } @@ -696,10 +703,25 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust ]) || []; } - const bucketResp = await mlClient.getBuckets({ - job_id: jobId, - body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } }, - }); + const { getAnnotations } = annotationServiceProvider(client!); + + const [bucketResp, annotationResp, { body: modelSnapshotsResp }] = await Promise.all([ + mlClient.getBuckets({ + job_id: jobId, + body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } }, + }), + getAnnotations({ + jobIds: [jobId], + earliestMs: start, + latestMs: end, + maxAnnotations: 1000, + }), + mlClient.getModelSnapshots({ + job_id: jobId, + start: String(start), + end: String(end), + }), + ]); const bucketResults = bucketResp?.body?.buckets ?? []; bucketResults.forEach((dataForTime) => { @@ -708,6 +730,36 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust finalResults.bucketResults.push([timestamp, eventCount]); }); + const annotationResults = annotationResp.annotations[jobId] || []; + annotationResults.forEach((annotation) => { + const timestamp = Number(annotation?.timestamp); + const endTimestamp = Number(annotation?.end_timestamp); + if (timestamp === endTimestamp) { + finalResults.annotationResultsLine.push({ + dataValue: timestamp, + details: annotation.annotation, + }); + } else { + finalResults.annotationResultsRect.push({ + coordinates: { + x0: timestamp, + x1: endTimestamp, + }, + details: annotation.annotation, + }); + } + }); + + const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? []; + modelSnapshots.forEach((modelSnapshot) => { + const timestamp = Number(modelSnapshot?.timestamp); + + finalResults.modelSnapshotResultsLine.push({ + dataValue: timestamp, + details: modelSnapshot.description, + }); + }); + return finalResults; } From 92c9a0e09656cb26951c81b9d35bb6ac8a0d529d Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 22 Jun 2021 19:37:08 -0400 Subject: [PATCH 084/118] [Rollups] Migrate to new page layout (#102268) (#102975) --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 2 + src/core/public/public.api.md | 1 + .../rollup/public/crud_app/_crud_app.scss | 8 - .../sections/job_create/job_create.js | 57 ++--- .../job_list/detail_panel/detail_panel.js | 2 +- .../detail_panel/detail_panel.test.js | 2 +- .../crud_app/sections/job_list/job_list.js | 214 +++++++++--------- .../sections/job_list/job_list.test.js | 26 ++- .../sections/job_list/job_table/job_table.js | 23 +- .../job_list/job_table/job_table.test.js | 8 + .../crud_app/store/actions/load_jobs.js | 13 +- .../plugins/rollup/public/shared_imports.ts | 6 +- .../test/client_integration/job_list.test.js | 5 +- .../client_integration/job_list_clone.test.js | 9 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 18 files changed, 198 insertions(+), 183 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index d3d76079cdc2a..ae433e3db14c6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -116,6 +116,7 @@ readonly links: { readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; + readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 34279cef198bf..b0800c7dfc65e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
    readonly canvas: {
    readonly guide: string;
    };
    readonly dashboard: {
    readonly guide: string;
    readonly drilldowns: string;
    readonly drilldownsTriggerPicker: string;
    readonly urlDrilldownTemplateSyntax: string;
    readonly urlDrilldownVariables: string;
    };
    readonly discover: Record<string, string>;
    readonly filebeat: {
    readonly base: string;
    readonly installation: string;
    readonly configuration: string;
    readonly elasticsearchOutput: string;
    readonly elasticsearchModule: string;
    readonly startup: string;
    readonly exportedFields: string;
    };
    readonly auditbeat: {
    readonly base: string;
    };
    readonly metricbeat: {
    readonly base: string;
    readonly configure: string;
    readonly httpEndpoint: string;
    readonly install: string;
    readonly start: string;
    };
    readonly enterpriseSearch: {
    readonly base: string;
    readonly appSearchBase: string;
    readonly workplaceSearchBase: string;
    };
    readonly heartbeat: {
    readonly base: string;
    };
    readonly logstash: {
    readonly base: string;
    };
    readonly functionbeat: {
    readonly base: string;
    };
    readonly winlogbeat: {
    readonly base: string;
    };
    readonly aggs: {
    readonly composite: string;
    readonly composite_missing_bucket: string;
    readonly date_histogram: string;
    readonly date_range: string;
    readonly date_format_pattern: string;
    readonly filter: string;
    readonly filters: string;
    readonly geohash_grid: string;
    readonly histogram: string;
    readonly ip_range: string;
    readonly range: string;
    readonly significant_terms: string;
    readonly terms: string;
    readonly avg: string;
    readonly avg_bucket: string;
    readonly max_bucket: string;
    readonly min_bucket: string;
    readonly sum_bucket: string;
    readonly cardinality: string;
    readonly count: string;
    readonly cumulative_sum: string;
    readonly derivative: string;
    readonly geo_bounds: string;
    readonly geo_centroid: string;
    readonly max: string;
    readonly median: string;
    readonly min: string;
    readonly moving_avg: string;
    readonly percentile_ranks: string;
    readonly serial_diff: string;
    readonly std_dev: string;
    readonly sum: string;
    readonly top_hits: string;
    };
    readonly runtimeFields: {
    readonly overview: string;
    readonly mapping: string;
    };
    readonly scriptedFields: {
    readonly scriptFields: string;
    readonly scriptAggs: string;
    readonly painless: string;
    readonly painlessApi: string;
    readonly painlessLangSpec: string;
    readonly painlessSyntax: string;
    readonly painlessWalkthrough: string;
    readonly luceneExpressions: string;
    };
    readonly search: {
    readonly sessions: string;
    };
    readonly indexPatterns: {
    readonly introduction: string;
    readonly fieldFormattersNumber: string;
    readonly fieldFormattersString: string;
    readonly runtimeFields: string;
    };
    readonly addData: string;
    readonly kibana: string;
    readonly upgradeAssistant: string;
    readonly elasticsearch: Record<string, string>;
    readonly siem: {
    readonly guide: string;
    readonly gettingStarted: string;
    };
    readonly query: {
    readonly eql: string;
    readonly kueryQuerySyntax: string;
    readonly luceneQuerySyntax: string;
    readonly percolate: string;
    readonly queryDsl: string;
    };
    readonly date: {
    readonly dateMath: string;
    readonly dateMathIndexNames: string;
    };
    readonly management: Record<string, string>;
    readonly ml: Record<string, string>;
    readonly transforms: Record<string, string>;
    readonly visualize: Record<string, string>;
    readonly apis: Readonly<{
    bulkIndexAlias: string;
    byteSizeUnits: string;
    createAutoFollowPattern: string;
    createFollower: string;
    createIndex: string;
    createSnapshotLifecyclePolicy: string;
    createRoleMapping: string;
    createRoleMappingTemplates: string;
    createRollupJobsRequest: string;
    createApiKey: string;
    createPipeline: string;
    createTransformRequest: string;
    cronExpressions: string;
    executeWatchActionModes: string;
    indexExists: string;
    openIndex: string;
    putComponentTemplate: string;
    painlessExecute: string;
    painlessExecuteAPIContexts: string;
    putComponentTemplateMetadata: string;
    putSnapshotLifecyclePolicy: string;
    putIndexTemplateV1: string;
    putWatch: string;
    simulatePipeline: string;
    timeUnits: string;
    updateTransform: string;
    }>;
    readonly observability: Record<string, string>;
    readonly alerting: Record<string, string>;
    readonly maps: Record<string, string>;
    readonly monitoring: Record<string, string>;
    readonly security: Readonly<{
    apiKeyServiceSettings: string;
    clusterPrivileges: string;
    elasticsearchSettings: string;
    elasticsearchEnableSecurity: string;
    indicesPrivileges: string;
    kibanaTLS: string;
    kibanaPrivileges: string;
    mappingRoles: string;
    mappingRolesFieldRules: string;
    runAsPrivilege: string;
    }>;
    readonly watcher: Record<string, string>;
    readonly ccs: Record<string, string>;
    readonly plugins: Record<string, string>;
    readonly snapshotRestore: Record<string, string>;
    readonly ingest: Record<string, string>;
    readonly fleet: Readonly<{
    guide: string;
    fleetServer: string;
    fleetServerAddFleetServer: string;
    settings: string;
    settingsFleetServerHostSettings: string;
    troubleshooting: string;
    elasticAgent: string;
    datastreams: string;
    datastreamsNamingScheme: string;
    upgradeElasticAgent: string;
    upgradeElasticAgent712lower: string;
    }>;
    } | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
    readonly canvas: {
    readonly guide: string;
    };
    readonly dashboard: {
    readonly guide: string;
    readonly drilldowns: string;
    readonly drilldownsTriggerPicker: string;
    readonly urlDrilldownTemplateSyntax: string;
    readonly urlDrilldownVariables: string;
    };
    readonly discover: Record<string, string>;
    readonly filebeat: {
    readonly base: string;
    readonly installation: string;
    readonly configuration: string;
    readonly elasticsearchOutput: string;
    readonly elasticsearchModule: string;
    readonly startup: string;
    readonly exportedFields: string;
    };
    readonly auditbeat: {
    readonly base: string;
    };
    readonly metricbeat: {
    readonly base: string;
    readonly configure: string;
    readonly httpEndpoint: string;
    readonly install: string;
    readonly start: string;
    };
    readonly enterpriseSearch: {
    readonly base: string;
    readonly appSearchBase: string;
    readonly workplaceSearchBase: string;
    };
    readonly heartbeat: {
    readonly base: string;
    };
    readonly logstash: {
    readonly base: string;
    };
    readonly functionbeat: {
    readonly base: string;
    };
    readonly winlogbeat: {
    readonly base: string;
    };
    readonly aggs: {
    readonly composite: string;
    readonly composite_missing_bucket: string;
    readonly date_histogram: string;
    readonly date_range: string;
    readonly date_format_pattern: string;
    readonly filter: string;
    readonly filters: string;
    readonly geohash_grid: string;
    readonly histogram: string;
    readonly ip_range: string;
    readonly range: string;
    readonly significant_terms: string;
    readonly terms: string;
    readonly avg: string;
    readonly avg_bucket: string;
    readonly max_bucket: string;
    readonly min_bucket: string;
    readonly sum_bucket: string;
    readonly cardinality: string;
    readonly count: string;
    readonly cumulative_sum: string;
    readonly derivative: string;
    readonly geo_bounds: string;
    readonly geo_centroid: string;
    readonly max: string;
    readonly median: string;
    readonly min: string;
    readonly moving_avg: string;
    readonly percentile_ranks: string;
    readonly serial_diff: string;
    readonly std_dev: string;
    readonly sum: string;
    readonly top_hits: string;
    };
    readonly runtimeFields: {
    readonly overview: string;
    readonly mapping: string;
    };
    readonly scriptedFields: {
    readonly scriptFields: string;
    readonly scriptAggs: string;
    readonly painless: string;
    readonly painlessApi: string;
    readonly painlessLangSpec: string;
    readonly painlessSyntax: string;
    readonly painlessWalkthrough: string;
    readonly luceneExpressions: string;
    };
    readonly search: {
    readonly sessions: string;
    };
    readonly indexPatterns: {
    readonly introduction: string;
    readonly fieldFormattersNumber: string;
    readonly fieldFormattersString: string;
    readonly runtimeFields: string;
    };
    readonly addData: string;
    readonly kibana: string;
    readonly upgradeAssistant: string;
    readonly rollupJobs: string;
    readonly elasticsearch: Record<string, string>;
    readonly siem: {
    readonly guide: string;
    readonly gettingStarted: string;
    };
    readonly query: {
    readonly eql: string;
    readonly kueryQuerySyntax: string;
    readonly luceneQuerySyntax: string;
    readonly percolate: string;
    readonly queryDsl: string;
    };
    readonly date: {
    readonly dateMath: string;
    readonly dateMathIndexNames: string;
    };
    readonly management: Record<string, string>;
    readonly ml: Record<string, string>;
    readonly transforms: Record<string, string>;
    readonly visualize: Record<string, string>;
    readonly apis: Readonly<{
    bulkIndexAlias: string;
    byteSizeUnits: string;
    createAutoFollowPattern: string;
    createFollower: string;
    createIndex: string;
    createSnapshotLifecyclePolicy: string;
    createRoleMapping: string;
    createRoleMappingTemplates: string;
    createRollupJobsRequest: string;
    createApiKey: string;
    createPipeline: string;
    createTransformRequest: string;
    cronExpressions: string;
    executeWatchActionModes: string;
    indexExists: string;
    openIndex: string;
    putComponentTemplate: string;
    painlessExecute: string;
    painlessExecuteAPIContexts: string;
    putComponentTemplateMetadata: string;
    putSnapshotLifecyclePolicy: string;
    putIndexTemplateV1: string;
    putWatch: string;
    simulatePipeline: string;
    timeUnits: string;
    updateTransform: string;
    }>;
    readonly observability: Record<string, string>;
    readonly alerting: Record<string, string>;
    readonly maps: Record<string, string>;
    readonly monitoring: Record<string, string>;
    readonly security: Readonly<{
    apiKeyServiceSettings: string;
    clusterPrivileges: string;
    elasticsearchSettings: string;
    elasticsearchEnableSecurity: string;
    indicesPrivileges: string;
    kibanaTLS: string;
    kibanaPrivileges: string;
    mappingRoles: string;
    mappingRolesFieldRules: string;
    runAsPrivilege: string;
    }>;
    readonly watcher: Record<string, string>;
    readonly ccs: Record<string, string>;
    readonly plugins: Record<string, string>;
    readonly snapshotRestore: Record<string, string>;
    readonly ingest: Record<string, string>;
    readonly fleet: Readonly<{
    guide: string;
    fleetServer: string;
    fleetServerAddFleetServer: string;
    settings: string;
    settingsFleetServerHostSettings: string;
    troubleshooting: string;
    elasticAgent: string;
    datastreams: string;
    datastreamsNamingScheme: string;
    upgradeElasticAgent: string;
    upgradeElasticAgent712lower: string;
    }>;
    } | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6a3e452a0bed8..c737f41cdabc5 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -137,6 +137,7 @@ export class DocLinksService { addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, kibana: `${KIBANA_DOCS}index.html`, upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, + rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, @@ -534,6 +535,7 @@ export interface DocLinksStart { readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; + readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6cc2b3f321fb7..27569935bcc65 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -595,6 +595,7 @@ export interface DocLinksStart { readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; + readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; diff --git a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss index 9e3bd491115ce..ddf69167145f1 100644 --- a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss +++ b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss @@ -4,11 +4,3 @@ .rollupJobWizardStepActions { align-items: flex-end; /* 1 */ } - -/** - * 1. Ensure panel fills width of parent when search input yields no matching rollup jobs. - */ -.rollupJobsListPanel { - // sass-lint:disable-block no-important - flex-grow: 1 !important; /* 1 */ -} diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js index 4ced63b0218f1..30aca6d5c0f5c 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { cloneDeep, debounce, first, mapValues } from 'lodash'; @@ -18,11 +18,10 @@ import { EuiCallOut, EuiLoadingKibana, EuiOverlayMask, - EuiPageContent, - EuiPageContentHeader, + EuiPageContentBody, + EuiPageHeader, EuiSpacer, EuiStepsHorizontal, - EuiTitle, } from '@elastic/eui'; import { @@ -523,44 +522,46 @@ export class JobCreateUi extends Component { } saveErrorFeedback = ( - + <> + + {errorBody} - + ); } return ( - - - - -

    - -

    -
    -
    - - {saveErrorFeedback} - - + + + } + /> - + + + + + {saveErrorFeedback} + + + + {this.renderCurrentStep()} - {this.renderCurrentStep()} + - + {this.renderNavigation()} - {this.renderNavigation()} -
    {savingFeedback} -
    + ); } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js index 4fe1674e8c643..5e97ff5e2980d 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js @@ -195,7 +195,7 @@ export class DetailPanel extends Component { diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js index 16919b8388e2e..e1f9ec2b3a315 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js @@ -70,7 +70,7 @@ describe('', () => { ({ component, find, exists } = initTestBed({ isLoading: true })); const loading = find('rollupJobDetailLoading'); expect(loading.length).toBeTruthy(); - expect(loading.text()).toEqual('Loading rollup job...'); + expect(loading.text()).toEqual('Loading rollup job…'); // Make sure the title and the tabs are visible expect(exists('detailPanelTabSelected')).toBeTruthy(); diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js index 589546a11ef38..b2448eb610774 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js @@ -12,24 +12,19 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, + EuiButtonEmpty, EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, + EuiPageHeader, EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, EuiSpacer, - EuiText, - EuiTextColor, - EuiTitle, - EuiCallOut, } from '@elastic/eui'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { extractQueryParams } from '../../../shared_imports'; +import { extractQueryParams, SectionLoading } from '../../../shared_imports'; import { getRouterLinkProps, listBreadcrumb } from '../../services'; +import { documentationLinks } from '../../services/documentation_links'; + import { JobTable } from './job_table'; import { DetailPanel } from './detail_panel'; @@ -87,38 +82,26 @@ export class JobListUi extends Component { this.props.closeDetailPanel(); } - getHeaderSection() { - return ( - - -

    - -

    -
    -
    - ); - } - renderNoPermission() { const title = i18n.translate('xpack.rollupJobs.jobList.noPermissionTitle', { defaultMessage: 'Permission error', }); return ( - - {this.getHeaderSection()} - - + - - - + iconType="alert" + title={

    {title}

    } + body={ +

    + +

    + } + /> + ); } @@ -130,101 +113,110 @@ export class JobListUi extends Component { const title = i18n.translate('xpack.rollupJobs.jobList.loadingErrorTitle', { defaultMessage: 'Error loading rollup jobs', }); + return ( - - {this.getHeaderSection()} - - - {statusCode} {errorString} - - + + {title}} + body={ +

    + {statusCode} {errorString} +

    + } + /> +
    ); } renderEmpty() { return ( - - - - } - body={ - -

    + + + + } + body={ + +

    + +

    +
    + } + actions={ + + -

    - - } - actions={ - - - - } - /> +
    + } + /> + ); } renderLoading() { return ( - - - - - - - - - - - - - + + + + + ); } renderList() { - const { isLoading } = this.props; - return ( - - - {this.getHeaderSection()} - - - + <> + + + + } + rightSideItems={[ + - - - + , + ]} + /> - {isLoading ? this.renderLoading() : } + + + - + ); } @@ -241,15 +233,13 @@ export class JobListUi extends Component { } } else if (!isLoading && !hasJobs) { content = this.renderEmpty(); + } else if (isLoading) { + content = this.renderLoading(); } else { content = this.renderList(); } - return ( - - {content} - - ); + return content; } } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js index 3283f4f521fc0..b2c738a033b3c 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js @@ -22,6 +22,15 @@ jest.mock('../../services', () => { }; }); +jest.mock('../../services/documentation_links', () => { + const coreMocks = jest.requireActual('../../../../../../../src/core/public/mocks'); + + return { + init: jest.fn(), + documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links, + }; +}); + const defaultProps = { history: { location: {} }, loadJobs: () => {}, @@ -52,14 +61,14 @@ describe('', () => { it('should display a loading message when loading the jobs', () => { const { component, exists } = initTestBed({ isLoading: true }); - expect(exists('jobListLoading')).toBeTruthy(); + expect(exists('sectionLoading')).toBeTruthy(); expect(component.find('JobTable').length).toBeFalsy(); }); it('should display the when there are jobs', () => { const { component, exists } = initTestBed({ hasJobs: true }); - expect(exists('jobListLoading')).toBeFalsy(); + expect(exists('sectionLoading')).toBeFalsy(); expect(component.find('JobTable').length).toBeTruthy(); }); @@ -71,21 +80,20 @@ describe('', () => { }, }); - it('should display a callout with the status and the message', () => { + it('should display an error with the status and the message', () => { expect(exists('jobListError')).toBeTruthy(); expect(find('jobListError').find('EuiText').text()).toEqual('400 Houston we got a problem.'); }); }); describe('when the user does not have the permission to access it', () => { - const { exists } = initTestBed({ jobLoadError: { status: 403 } }); + const { exists, find } = initTestBed({ jobLoadError: { status: 403 } }); - it('should render a callout message', () => { + it('should render an error message', () => { expect(exists('jobListNoPermission')).toBeTruthy(); - }); - - it('should display the page header', () => { - expect(exists('jobListPageHeader')).toBeTruthy(); + expect(find('jobListNoPermission').find('EuiText').text()).toEqual( + 'You do not have permission to view or add rollup jobs.' + ); }); }); }); diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index fe3d2cbd4cbe0..83135cf219f35 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -28,10 +28,11 @@ import { EuiTableRowCellCheckbox, EuiText, EuiToolTip, + EuiButton, } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common'; -import { METRIC_TYPE } from '../../../services'; +import { METRIC_TYPE, getRouterLinkProps } from '../../../services'; import { trackUiMetric } from '../../../../kibana_services'; import { JobActionMenu, JobStatus } from '../../components'; @@ -346,9 +347,9 @@ export class JobTable extends Component { const atLeastOneItemSelected = Object.keys(idToSelectedJobMap).length > 0; return ( - - - {atLeastOneItemSelected ? ( +
    + + {atLeastOneItemSelected && ( - ) : null} + )} + + + + + @@ -409,7 +418,7 @@ export class JobTable extends Component { {jobs.length > 0 ? this.renderPager() : null} - +
    ); } } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js index 3fa879923c40a..d52f3fa35a544 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js @@ -20,6 +20,14 @@ jest.mock('../../../../kibana_services', () => { }; }); +jest.mock('../../../services', () => { + const services = jest.requireActual('../../../services'); + return { + ...services, + getRouterLinkProps: (link) => ({ href: link }), + }; +}); + const defaultProps = { jobs: [], pager: new Pager(20, 10, 1), diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js index 0dc3a02d3c077..c63d01f3c200d 100644 --- a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js +++ b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js @@ -5,9 +5,7 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - -import { loadJobs as sendLoadJobsRequest, deserializeJobs, showApiError } from '../../services'; +import { loadJobs as sendLoadJobsRequest, deserializeJobs } from '../../services'; import { LOAD_JOBS_START, LOAD_JOBS_SUCCESS, LOAD_JOBS_FAILURE } from '../action_types'; export const loadJobs = () => async (dispatch) => { @@ -19,17 +17,10 @@ export const loadJobs = () => async (dispatch) => { try { jobs = await sendLoadJobsRequest(); } catch (error) { - dispatch({ + return dispatch({ type: LOAD_JOBS_FAILURE, payload: { error }, }); - - return showApiError( - error, - i18n.translate('xpack.rollupJobs.loadAction.errorTitle', { - defaultMessage: 'Error loading rollup jobs', - }) - ); } dispatch({ diff --git a/x-pack/plugins/rollup/public/shared_imports.ts b/x-pack/plugins/rollup/public/shared_imports.ts index fd28175318666..c8d7f1d9f13f3 100644 --- a/x-pack/plugins/rollup/public/shared_imports.ts +++ b/x-pack/plugins/rollup/public/shared_imports.ts @@ -5,4 +5,8 @@ * 2.0. */ -export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public'; +export { + extractQueryParams, + indices, + SectionLoading, +} from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js index fa1a786bc8a71..46ddfbcfc2de5 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js @@ -5,10 +5,10 @@ * 2.0. */ -import { getRouter, setHttp } from '../../crud_app/services'; +import { getRouter, setHttp, init as initDocumentation } from '../../crud_app/services'; import { mockHttpRequest, pageHelpers, nextTick } from './helpers'; import { JOBS } from './helpers/constants'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks'; jest.mock('../../crud_app/services', () => { const services = jest.requireActual('../../crud_app/services'); @@ -38,6 +38,7 @@ describe('', () => { beforeAll(() => { startMock = coreMock.createStart(); setHttp(startMock.http); + initDocumentation(docLinksServiceMock.createStartContract()); }); beforeEach(async () => { diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js index cfb63893ee423..3987e18538e57 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js @@ -24,6 +24,15 @@ jest.mock('../../kibana_services', () => { }; }); +jest.mock('../../crud_app/services/documentation_links', () => { + const coreMocks = jest.requireActual('../../../../../../src/core/public/mocks'); + + return { + init: jest.fn(), + documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links, + }; +}); + const { setup } = pageHelpers.jobList; describe('Smoke test cloning an existing rollup job from job list', () => { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 955e4f890e511..85916f8ac30db 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18015,7 +18015,6 @@ "xpack.rollupJobs.jobTable.selectRow": "この行 {id} を選択", "xpack.rollupJobs.licenseCheckErrorMessage": "ライセンス確認失敗", "xpack.rollupJobs.listBreadcrumbTitle": "ロールアップジョブ", - "xpack.rollupJobs.loadAction.errorTitle": "ロールアップジョブを読み込み中にエラーが発生", "xpack.rollupJobs.refreshAction.errorTitle": "ロールアップジョブの更新中にエラーが発生", "xpack.rollupJobs.rollupIndexPatternsDescription": "ロールアップインデックスを捕捉するインデックスパターンの作成を有効にします。\n それによりロールアップデータに基づくビジュアライゼーションが可能になります。", "xpack.rollupJobs.rollupIndexPatternsTitle": "ロールアップインデックスパターンを有効にする", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c629b1495a602..23ddc4e293ac0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18259,7 +18259,6 @@ "xpack.rollupJobs.jobTable.selectRow": "选择行 {id}", "xpack.rollupJobs.licenseCheckErrorMessage": "许可证检查失败", "xpack.rollupJobs.listBreadcrumbTitle": "汇总/打包作业", - "xpack.rollupJobs.loadAction.errorTitle": "加载汇总/打包作业时出错", "xpack.rollupJobs.refreshAction.errorTitle": "刷新汇总/打包作业时出错", "xpack.rollupJobs.rollupIndexPatternsDescription": "启用用于捕获汇总/打包索引的索引模式的创建,\n 汇总/打包索引反过来基于汇总/打包数据启用可视化。", "xpack.rollupJobs.rollupIndexPatternsTitle": "启用汇总索引模式", From 074c00849f1419c7ba8774e3f4453825cde873d4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 19:50:12 -0400 Subject: [PATCH 085/118] [App Search] Convert Result Settings & Relevance Tuning pages to new page template (#102845) (#103018) * Convert Result Settings page to new page template + remove wrapper around empty state (auto handled by new page template) + update tests w/ new test helpers * Convert Relevance Tuning page to new page template - Remove old relevance_tuning_layout (which handled breadcrumbs, page header, flash messages, and callouts) in favor of simply using the new templtate + callouts (yay DRYing) - Remove panel wrapper around empty state (handled by new page template) * Update router * [Polish] Spacing & icon polish from Davey see https://github.com/elastic/kibana/pull/101958/files Co-authored-by: Constance --- .../components/engine/engine_router.tsx | 20 ++-- .../components/empty_state.tsx | 62 ++++++----- .../relevance_tuning.test.tsx | 57 +++++----- .../relevance_tuning/relevance_tuning.tsx | 74 +++++++++---- .../relevance_tuning_form.tsx | 2 +- .../relevance_tuning_layout.test.tsx | 64 ----------- .../relevance_tuning_layout.tsx | 73 ------------- .../relevance_tuning_preview.tsx | 1 + .../components/empty_state.tsx | 62 ++++++----- .../result_settings/result_settings.test.tsx | 56 ++++------ .../result_settings/result_settings.tsx | 101 +++++++++--------- 11 files changed, 224 insertions(+), 348 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx 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 91a21847107a9..04e252e44270b 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 @@ -114,6 +114,16 @@ export const EngineRouter: React.FC = () => { )} + {canManageEngineRelevanceTuning && ( + + + + )} + {canManageEngineResultSettings && ( + + + + )} {canManageEngineSearchUi && ( @@ -131,21 +141,11 @@ export const EngineRouter: React.FC = () => { )} - {canManageEngineRelevanceTuning && ( - - - - )} {canManageEngineSynonyms && ( )} - {canManageEngineResultSettings && ( - - - - )} {canViewMetaEngineSourceEngines && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx index e6a14d7b5cd72..df29010bd682f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx @@ -7,42 +7,40 @@ import React from 'react'; -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; export const EmptyState: React.FC = () => ( - - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', { - defaultMessage: 'Add documents to tune relevance', - })} - + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', { + defaultMessage: 'Add documents to tune relevance', + })} + + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description', + { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', } - body={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description', - { - defaultMessage: - 'A schema will be automatically created for you after you index some documents.', - } - )} - actions={ - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel', - { defaultMessage: 'Read the relevance tuning guide' } - )} - - } - /> - + )} + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel', + { defaultMessage: 'Read the relevance tuning guide' } + )} + + } + /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx index 092740ac5d3cc..48b536a954ed5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx @@ -13,14 +13,14 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiEmptyPrompt } from '@elastic/eui'; - -import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { getPageHeaderActions } from '../../../test_helpers'; -import { EmptyState } from './components'; import { RelevanceTuning } from './relevance_tuning'; + +import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; import { RelevanceTuningForm } from './relevance_tuning_form'; +import { RelevanceTuningPreview } from './relevance_tuning_preview'; describe('RelevanceTuning', () => { const values = { @@ -50,9 +50,9 @@ describe('RelevanceTuning', () => { it('renders', () => { const wrapper = subject(); + expect(wrapper.find(RelevanceTuningCallouts).exists()).toBe(true); expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true); - expect(wrapper.find(Loading).exists()).toBe(false); - expect(wrapper.find(EmptyState).exists()).toBe(false); + expect(wrapper.find(RelevanceTuningPreview).exists()).toBe(true); }); it('initializes relevance tuning data', () => { @@ -60,33 +60,38 @@ describe('RelevanceTuning', () => { expect(actions.initializeRelevanceTuning).toHaveBeenCalled(); }); - it('will render an empty message when the engine has no schema', () => { + it('will prevent user from leaving the page if there are unsaved changes', () => { setMockValues({ ...values, - engineHasSchemaFields: false, + unsavedChanges: true, }); - const wrapper = subject(); - expect(wrapper.find(EmptyState).dive().find(EuiEmptyPrompt).exists()).toBe(true); - expect(wrapper.find(Loading).exists()).toBe(false); - expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); + expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true); }); - it('will show a loading message if data is loading', () => { - setMockValues({ - ...values, - dataLoading: true, + describe('header actions', () => { + it('renders a Save button that will save the current changes', () => { + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(2); + const saveButton = buttons.find('[data-test-subj="SaveRelevanceTuning"]'); + saveButton.simulate('click'); + expect(actions.updateSearchSettings).toHaveBeenCalled(); }); - const wrapper = subject(); - expect(wrapper.find(Loading).exists()).toBe(true); - expect(wrapper.find(EmptyState).exists()).toBe(false); - expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); - }); - it('will prevent user from leaving the page if there are unsaved changes', () => { - setMockValues({ - ...values, - unsavedChanges: true, + it('renders a Reset button that will remove all weights and boosts', () => { + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(2); + const resetButton = buttons.find('[data-test-subj="ResetRelevanceTuning"]'); + resetButton.simulate('click'); + expect(actions.resetSearchSettings).toHaveBeenCalled(); + }); + + it('will not render buttons if the engine has no schema', () => { + setMockValues({ + ...values, + engineHasSchemaFields: false, + }); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(0); }); - expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true); }); }); 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 b98541a963890..2e87d6836199b 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 @@ -9,43 +9,77 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { Loading } from '../../../shared/loading'; +import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { EmptyState } from './components'; +import { RELEVANCE_TUNING_TITLE } from './constants'; +import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; import { RelevanceTuningForm } from './relevance_tuning_form'; -import { RelevanceTuningLayout } from './relevance_tuning_layout'; import { RelevanceTuningPreview } from './relevance_tuning_preview'; import { RelevanceTuningLogic } from '.'; export const RelevanceTuning: React.FC = () => { const { dataLoading, engineHasSchemaFields, unsavedChanges } = useValues(RelevanceTuningLogic); - const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic); + const { initializeRelevanceTuning, resetSearchSettings, updateSearchSettings } = useActions( + RelevanceTuningLogic + ); useEffect(() => { initializeRelevanceTuning(); }, []); - if (dataLoading) return ; - return ( - + + {SAVE_BUTTON_LABEL} + , + + {RESTORE_DEFAULTS_BUTTON_LABEL} + , + ] + : [], + }} + isLoading={dataLoading} + isEmptyState={!engineHasSchemaFields} + emptyState={} + > - {engineHasSchemaFields ? ( - - - - - - - - - ) : ( - - )} - + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx index 5cbd291f85deb..c35cd280c7a05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx @@ -42,7 +42,7 @@ export const RelevanceTuningForm: React.FC = () => { return (
    - +

    {i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx deleted file mode 100644 index 20b1a16879234..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; -import '../../__mocks__/engine_logic.mock'; - -import React from 'react'; - -import { shallow, ShallowWrapper } from 'enzyme'; - -import { EuiPageHeader } from '@elastic/eui'; - -import { RelevanceTuningLayout } from './relevance_tuning_layout'; - -describe('RelevanceTuningLayout', () => { - const values = { - engineHasSchemaFields: true, - schemaFieldsWithConflicts: [], - }; - - const actions = { - updateSearchSettings: jest.fn(), - resetSearchSettings: jest.fn(), - }; - - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(values); - setMockActions(actions); - }); - - const subject = () => shallow(); - const findButtons = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; - - it('renders a Save button that will save the current changes', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(2); - const saveButton = shallow(buttons[0]); - saveButton.simulate('click'); - expect(actions.updateSearchSettings).toHaveBeenCalled(); - }); - - it('renders a Reset button that will remove all weights and boosts', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(2); - const resetButton = shallow(buttons[1]); - resetButton.simulate('click'); - expect(actions.resetSearchSettings).toHaveBeenCalled(); - }); - - it('will not render buttons if the engine has no schema', () => { - setMockValues({ - ...values, - engineHasSchemaFields: false, - }); - const buttons = findButtons(subject()); - expect(buttons.length).toBe(0); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx deleted file mode 100644 index 4fa694300a779..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx +++ /dev/null @@ -1,73 +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 { useActions, useValues } from 'kea'; - -import { EuiPageHeader, EuiButton } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; -import { getEngineBreadcrumbs } from '../engine'; - -import { RELEVANCE_TUNING_TITLE } from './constants'; -import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; -import { RelevanceTuningLogic } from './relevance_tuning_logic'; - -export const RelevanceTuningLayout: React.FC = ({ children }) => { - const { resetSearchSettings, updateSearchSettings } = useActions(RelevanceTuningLogic); - const { engineHasSchemaFields } = useValues(RelevanceTuningLogic); - - const pageHeader = () => ( - - {SAVE_BUTTON_LABEL} - , - - {RESTORE_DEFAULTS_BUTTON_LABEL} - , - ] - : [] - } - /> - ); - - return ( - <> - - {pageHeader()} - - - {children} - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx index 911e97de5b53f..4f3b20b419e80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx @@ -21,6 +21,7 @@ import { RelevanceTuningLogic } from '.'; const emptyCallout = ( ( - - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.title', { - defaultMessage: 'Add documents to adjust settings', - })} -

    + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.title', { + defaultMessage: 'Add documents to adjust settings', + })} + + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.description', + { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', } - body={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.description', - { - defaultMessage: - 'A schema will be automatically created for you after you index some documents.', - } - )} - actions={ - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.buttonLabel', - { defaultMessage: 'Read the result settings guide' } - )} - - } - /> - + )} + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.buttonLabel', + { defaultMessage: 'Read the result settings guide' } + )} + + } + /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index ec521b4959535..440acaf136dda 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -13,11 +13,9 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { getPageHeaderActions } from '../../../test_helpers'; -import { EmptyState } from './components'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; import { SampleResponse } from './sample_response'; @@ -46,8 +44,6 @@ describe('ResultSettings', () => { }); const subject = () => shallow(); - const findButtons = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; it('renders', () => { const wrapper = subject(); @@ -60,19 +56,10 @@ describe('ResultSettings', () => { expect(actions.initializeResultSettingsData).toHaveBeenCalled(); }); - it('renders a loading screen if data has not loaded yet', () => { - setMockValues({ - dataLoading: true, - }); - const wrapper = subject(); - expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); - expect(wrapper.find(SampleResponse).exists()).toBe(false); - }); - it('renders a "save" button that will save the current changes', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(3); - const saveButton = shallow(buttons[0]); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(3); + const saveButton = buttons.find('[data-test-subj="SaveResultSettings"]'); saveButton.simulate('click'); expect(actions.saveResultSettings).toHaveBeenCalled(); }); @@ -82,8 +69,8 @@ describe('ResultSettings', () => { ...values, stagedUpdates: false, }); - const buttons = findButtons(subject()); - const saveButton = shallow(buttons[0]); + const buttons = getPageHeaderActions(subject()); + const saveButton = buttons.find('[data-test-subj="SaveResultSettings"]'); expect(saveButton.prop('disabled')).toBe(true); }); @@ -93,15 +80,15 @@ describe('ResultSettings', () => { stagedUpdates: true, resultFieldsEmpty: true, }); - const buttons = findButtons(subject()); - const saveButton = shallow(buttons[0]); + const buttons = getPageHeaderActions(subject()); + const saveButton = buttons.find('[data-test-subj="SaveResultSettings"]'); expect(saveButton.prop('disabled')).toBe(true); }); it('renders a "restore defaults" button that will reset all values to their defaults', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(3); - const resetButton = shallow(buttons[1]); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(3); + const resetButton = buttons.find('[data-test-subj="ResetResultSettings"]'); resetButton.simulate('click'); expect(actions.confirmResetAllFields).toHaveBeenCalled(); }); @@ -111,15 +98,15 @@ describe('ResultSettings', () => { ...values, resultFieldsAtDefaultSettings: true, }); - const buttons = findButtons(subject()); - const resetButton = shallow(buttons[1]); + const buttons = getPageHeaderActions(subject()); + const resetButton = buttons.find('[data-test-subj="ResetResultSettings"]'); expect(resetButton.prop('disabled')).toBe(true); }); it('renders a "clear" button that will remove all selected options', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(3); - const clearButton = shallow(buttons[2]); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(3); + const clearButton = buttons.find('[data-test-subj="ClearResultSettings"]'); clearButton.simulate('click'); expect(actions.clearAllFields).toHaveBeenCalled(); }); @@ -143,17 +130,12 @@ describe('ResultSettings', () => { }); it('will not render action buttons', () => { - const buttons = findButtons(wrapper); - expect(buttons.length).toBe(0); - }); - - it('will not render the main page content', () => { - expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); - expect(wrapper.find(SampleResponse).exists()).toBe(false); + const buttons = getPageHeaderActions(wrapper); + expect(buttons.children().length).toBe(0); }); it('will render an empty state', () => { - expect(wrapper.find(EmptyState).exists()).toBe(true); + expect(wrapper.prop('isEmptyState')).toBe(true); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 45cb9ea1cfcb4..c315927433a0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -9,17 +9,15 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { EmptyState } from './components'; import { RESULT_SETTINGS_TITLE } from './constants'; @@ -57,59 +55,56 @@ export const ResultSettings: React.FC = () => { initializeResultSettingsData(); }, []); - if (dataLoading) return ; const hasSchema = Object.keys(schema).length > 0; return ( - <> - - - - {SAVE_BUTTON_LABEL} - , - - {RESTORE_DEFAULTS_BUTTON_LABEL} - , - - {CLEAR_BUTTON_LABEL} - , - ] - : [] - } - /> - - {hasSchema ? ( - - - - - - - - - ) : ( - - )} - + ), + rightSideItems: hasSchema + ? [ + + {SAVE_BUTTON_LABEL} + , + + {RESTORE_DEFAULTS_BUTTON_LABEL} + , + + {CLEAR_BUTTON_LABEL} + , + ] + : [], + }} + isLoading={dataLoading} + isEmptyState={!hasSchema} + emptyState={} + > + + + + + + + + + + + ); }; From 25aee7221dcf326b95df0d8d3d3e2680e5db27d9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 19:52:15 -0400 Subject: [PATCH 086/118] [App Search][Polish] API Logs empty state (#102998) (#103019) * Re-add noItemsMessage to ApiLogsTable - Primarily for Engine Overview use - totally forgot about this :facepalm: * Tweak API logs empty state copy - after discussing w/ Davey Co-authored-by: Constance --- .../components/api_logs/components/api_logs_table.tsx | 3 +++ .../components/api_logs/components/empty_state.test.tsx | 2 +- .../app_search/components/api_logs/components/empty_state.tsx | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx index 1b5a8084f5b59..d5bb525cfd332 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -26,6 +26,8 @@ import { ApiLogsLogic } from '../index'; import { ApiLog } from '../types'; import { getStatusColor } from '../utils'; +import { EmptyState } from './'; + import './api_logs_table.scss'; interface Props { @@ -108,6 +110,7 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => { items={apiLogs} responsive loading={dataLoading} + noItemsMessage={} {...paginationProps} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx index 3ad22ceac5840..19f45ced5dc5d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx @@ -19,7 +19,7 @@ describe('EmptyState', () => { .find(EuiEmptyPrompt) .dive(); - expect(wrapper.find('h2').text()).toEqual('Perform your first API call'); + expect(wrapper.find('h2').text()).toEqual('No API events in the last 24 hours'); expect(wrapper.find(EuiButton).prop('href')).toEqual( expect.stringContaining('/api-reference.html') ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx index 3f6f44adefc71..76bd0cba1731f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx @@ -18,14 +18,14 @@ export const EmptyState: React.FC = () => ( title={

    {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', { - defaultMessage: 'Perform your first API call', + defaultMessage: 'No API events in the last 24 hours', })}

    } body={

    {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', { - defaultMessage: "Check back after you've performed some API calls.", + defaultMessage: 'Logs will update in real-time when an API request occurs.', })}

    } From d3ed1794070fe2a6a4415e98f3f8be9425350559 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 20:13:29 -0400 Subject: [PATCH 087/118] [APM] Fix bug when error page is empty (#102940) (#103022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Søren Louv-Jansen --- .../Distribution/index.stories.tsx | 81 +++++++++++++++++++ .../ErrorGroupDetails/Distribution/index.tsx | 7 +- 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx new file mode 100644 index 0000000000000..8cc16dd801c25 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx @@ -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. + */ + +import React, { ComponentType } from 'react'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + ApmPluginContext, + ApmPluginContextValue, +} from '../../../../context/apm_plugin/apm_plugin_context'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import { ErrorDistribution } from './'; + +export default { + title: 'app/ErrorGroupDetails/Distribution', + component: ErrorDistribution, + decorators: [ + (Story: ComponentType) => { + const apmPluginContextMock = ({ + observabilityRuleTypeRegistry: { getFormatter: () => undefined }, + } as unknown) as ApmPluginContextValue; + + const kibanaContextServices = { + uiSettings: { get: () => {} }, + }; + + return ( + + + + + + + + ); + }, + ], +}; + +export function Example() { + const distribution = { + noHits: false, + bucketSize: 62350, + buckets: [ + { key: 1624279912350, count: 6 }, + { key: 1624279974700, count: 1 }, + { key: 1624280037050, count: 2 }, + { key: 1624280099400, count: 3 }, + { key: 1624280161750, count: 13 }, + { key: 1624280224100, count: 1 }, + { key: 1624280286450, count: 2 }, + { key: 1624280348800, count: 0 }, + { key: 1624280411150, count: 4 }, + { key: 1624280473500, count: 4 }, + { key: 1624280535850, count: 1 }, + { key: 1624280598200, count: 4 }, + { key: 1624280660550, count: 0 }, + { key: 1624280722900, count: 2 }, + { key: 1624280785250, count: 3 }, + { key: 1624280847600, count: 0 }, + ], + }; + + return ; +} + +export function EmptyState() { + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 643653c24aeb3..e53aaf97cdf75 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -67,6 +67,7 @@ export function ErrorDistribution({ distribution, title }: Props) { const xFormatter = niceTimeFormatter([xMin, xMax]); const { observabilityRuleTypeRegistry } = useApmPluginContext(); + const { alerts } = useApmServiceContext(); const { getFormatter } = observabilityRuleTypeRegistry; const [selectedAlertId, setSelectedAlertId] = useState( @@ -84,7 +85,7 @@ export function ErrorDistribution({ distribution, title }: Props) { }; return ( -
    + <> {title} @@ -124,7 +125,7 @@ export function ErrorDistribution({ distribution, title }: Props) { alerts: alerts?.filter( (alert) => alert[RULE_ID]?.[0] === AlertType.ErrorCount ), - chartStartTime: buckets[0].x0, + chartStartTime: buckets[0]?.x0, getFormatter, selectedAlertId, setSelectedAlertId, @@ -143,6 +144,6 @@ export function ErrorDistribution({ distribution, title }: Props) {
    -
    + ); } From d4019b843b615024e7b7c8181ba2e9b516c7f2d5 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Tue, 22 Jun 2021 22:18:08 -0400 Subject: [PATCH 088/118] Replacing es_archives/reporting/ecommerce_kibana with kbn_archiver/reporting/ecommerce.json as part of migrating to kbn_archiver (#102825) (#103027) # Conflicts: # x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json # x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json --- .../apps/dashboard/reporting/download_csv.ts | 6 +- .../apps/dashboard/reporting/screenshots.ts | 10 +- .../functional/apps/discover/reporting.ts | 11 +- .../apps/discover/saved_searches.ts | 5 +- .../functional/apps/visualize/reporting.ts | 9 +- .../reporting/ecommerce_kibana/data.json | 726 ----- .../reporting/ecommerce_kibana/mappings.json | 2679 ----------------- .../kbn_archiver/reporting/ecommerce.json | 678 +++++ .../services/scenarios.ts | 6 +- .../reporting_without_security/management.ts | 7 +- 10 files changed, 709 insertions(+), 3428 deletions(-) delete mode 100644 x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json delete mode 100644 x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index e736fe08eba99..94540aa8b4c46 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -17,10 +17,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const log = getService('log'); const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); const filterBar = getService('filterBar'); const find = getService('find'); const retry = getService('retry'); const PageObjects = getPageObjects(['reporting', 'common', 'dashboard', 'timePicker']); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; const getCsvPath = (name: string) => path.resolve(REPO_ROOT, `target/functional-tests/downloads/${name}.csv`); @@ -67,11 +69,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('E-Commerce Data', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); }); it('Download CSV export of a saved search panel', async function () { diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts index 7c5e4b2d12baa..7eb2ef74000e0 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts @@ -27,13 +27,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const es = getService('es'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; - describe('Dashboard Reporting Screenshots', () => { + // https://github.com/elastic/kibana/issues/102911 + describe.skip('Dashboard Reporting Screenshots', () => { before('initialize tests', async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.loadIfNeeded( - 'x-pack/test/functional/es_archives/reporting/ecommerce_kibana' - ); + await kibanaServer.importExport.load(ecommerceSOPath); await browser.setWindowSize(1600, 850); await security.role.create('test_reporting_user', { @@ -61,7 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await es.deleteByQuery({ index: '.reporting-*', refresh: true, diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 2b424b94b7236..3eb66204df564 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects(['reporting', 'common', 'discover', 'timePicker']); const filterBar = getService('filterBar'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; const setFieldsFromSource = async (setValue: boolean) => { await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': setValue }); @@ -25,12 +26,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); await browser.setWindowSize(1600, 850); }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await es.deleteByQuery({ index: '.reporting-*', refresh: true, @@ -74,7 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Generate CSV: new search', () => { beforeEach(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); // reload the archive to wipe out changes made by each test + await kibanaServer.importExport.load(ecommerceSOPath); await PageObjects.common.navigateToApp('discover'); }); @@ -151,12 +152,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); }); beforeEach(() => PageObjects.common.navigateToApp('discover')); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 5df9bf9949128..1d8de9fe9fb6d 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -16,16 +16,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const panelActions = getService('dashboardPanelActions'); const panelActionsTimeRange = getService('dashboardPanelTimeRange'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; describe('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); await kibanaServer.uiSettings.update({ 'doc_table:legacy': false }); }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await kibanaServer.uiSettings.unset('doc_table:legacy'); }); diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts index 799006337300f..c43747c346ca7 100644 --- a/x-pack/test/functional/apps/visualize/reporting.ts +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -13,6 +13,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); + const kibanaServer = getService('kibanaServer'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; + const PageObjects = getPageObjects([ 'reporting', 'common', @@ -25,14 +28,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.loadIfNeeded( - 'x-pack/test/functional/es_archives/reporting/ecommerce_kibana' - ); + await kibanaServer.importExport.load(ecommerceSOPath); await browser.setWindowSize(1600, 850); }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await es.deleteByQuery({ index: '.reporting-*', refresh: true, diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json deleted file mode 100644 index d64ba9595b7bc..0000000000000 --- a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json +++ /dev/null @@ -1,726 +0,0 @@ -{ - "type": "doc", - "value": { - "id": "config:7.0.0", - "index": ".kibana_1", - "source": { - "config": { - "buildNum": 9007199254740991, - "dateFormat:tz": "UTC", - "defaultIndex": "5193f870-d861-11e9-a311-0fa548c5f953" - }, - "references": [], - "type": "config", - "updated_at": "2019-09-16T09:06:51.201Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "index-pattern:5193f870-d861-11e9-a311-0fa548c5f953", - "index": ".kibana_1", - "source": { - "index-pattern": { - "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"category\"}}},{\"name\":\"currency\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_birth_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_first_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_first_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_first_name\"}}},{\"name\":\"customer_full_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_full_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_full_name\"}}},{\"name\":\"customer_gender\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_last_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_last_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_last_name\"}}},{\"name\":\"customer_phone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week_i\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"manufacturer\"}}},{\"name\":\"order_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"order_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products._id\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products._id.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products._id\"}}},{\"name\":\"products.base_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.base_unit_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.category\"}}},{\"name\":\"products.created_on\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_percentage\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.manufacturer\"}}},{\"name\":\"products.min_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.product_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.product_name\"}}},{\"name\":\"products.quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.tax_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxful_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxless_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.unit_discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxful_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxless_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_unique_products\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "timeFieldName": "order_date", - "title": "ecommerce" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-12-11T23:24:13.381Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "search:6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "index": ".kibana_1", - "source": { - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "search": { - "columns": [ - "order_date", - "category", - "currency", - "customer_id", - "order_id", - "day_of_week_i", - "products.created_on", - "sku" - ], - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "sort": [ - [ - "order_date", - "desc" - ] - ], - "title": "Ecommerce Data", - "version": 1 - }, - "type": "search", - "updated_at": "2019-12-11T23:24:28.540Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "dashboard:constructed-sample-saved-object-id", - "index": ".kibana_1", - "source": { - "dashboard": { - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" - }, - "optionsJSON": "{\"hidePanelTitles\":true,\"useMargins\":true}", - "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":33,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":41,\"w\":11,\"h\":10,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":11,\"y\":41,\"w\":5,\"h\":10,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]", - "refreshInterval": { - "pause": true, - "value": 0 - }, - "timeFrom": "2019-06-26T06:20:28.066Z", - "timeRestore": true, - "timeTo": "2019-06-26T07:27:58.573Z", - "title": "Ecom Dashboard Hidden Panel Titles", - "version": 1 - }, - "references": [ - { - "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_0", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_1", - "type": "visualization" - }, - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "panel_2", - "type": "search" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "panel_4", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_5", - "type": "visualization" - } - ], - "type": "dashboard", - "updated_at": "2020-04-10T00:37:48.462Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "index": ".kibana_1", - "source": { - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-08T23:24:05.971Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "e-commerce area chart", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"title\":\"e-commerce area chart\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "index": ".kibana_1", - "source": { - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-08T23:24:42.460Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "e-commerce pie chart", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"title\":\"e-commerce pie chart\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "index": ".kibana_1", - "source": { - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-10T00:33:44.909Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" - }, - "title": "게이지", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"gauge\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"rgba(105,112,125,0.2)\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"rgba(105,112,125,0.2)\",\"bgColor\":true,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"게이지\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "index": ".kibana_1", - "source": { - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-10T00:34:44.700Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" - }, - "title": "Українська", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"metric\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"Українська\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "index": ".kibana_1", - "source": { - "references": [], - "type": "visualization", - "updated_at": "2020-04-10T00:36:17.053Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "title": "Tiểu thuyết", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"markdown\",\"aggs\":[],\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Tiểu thuyết là một thể loại văn xuôi có hư cấu, thông qua nhân vật, hoàn cảnh, sự việc để phản ánh bức tranh xã hội rộng lớn và những vấn đề của cuộc sống con người, biểu hiện tính chất tường thuật, tính chất kể chuyện bằng ngôn ngữ văn xuôi theo những chủ đề xác định.\\n\\nTrong một cách hiểu khác, nhận định của Belinski: \\\"tiểu thuyết là sử thi của đời tư\\\" chỉ ra khái quát nhất về một dạng thức tự sự, trong đó sự trần thuật tập trung vào số phận của một cá nhân trong quá trình hình thành và phát triển của nó. Sự trần thuật ở đây được khai triển trong không gian và thời gian nghệ thuật đến mức đủ để truyền đạt cơ cấu của nhân cách[1].\\n\\n\\n[1]^ Mục từ Tiểu thuyết trong cuốn 150 thuật ngữ văn học, Lại Nguyên Ân biên soạn, Nhà xuất bản Đại học Quốc gia Hà Nội, in lần thứ 2 có sửa đổi bổ sung. H. 2003. Trang 326.\"},\"title\":\"Tiểu thuyết\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "space:default", - "index": ".kibana_1", - "source": { - "space": { - "_reserved": true, - "description": "This is the default space", - "disabledFeatures": [], - "name": "Default Space" - }, - "type": "space", - "updated_at": "2021-01-07T00:17:12.785Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "dashboard:6c263e00-1c6d-11ea-a100-8589bb9d7c6b", - "index": ".kibana_1", - "source": { - "dashboard": { - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" - }, - "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", - "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":35,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":23,\"w\":16,\"h\":12,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":16,\"y\":23,\"w\":12,\"h\":12,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":28,\"y\":23,\"w\":20,\"h\":12,\"i\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\"},\"panelIndex\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]", - "refreshInterval": { - "pause": true, - "value": 0 - }, - "timeFrom": "2019-03-23T03:06:17.785Z", - "timeRestore": true, - "timeTo": "2019-10-04T02:33:16.708Z", - "title": "Ecom Dashboard", - "version": 1 - }, - "references": [ - { - "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_0", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_1", - "type": "visualization" - }, - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "panel_2", - "type": "search" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "panel_4", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_5", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "panel_6", - "type": "visualization" - } - ], - "type": "dashboard", - "updated_at": "2021-01-07T00:22:16.102Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:1bba55f0-507e-11eb-9c0d-97106882b997", - "index": ".kibana_1", - "source": { - "references": [ - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "search_0", - "type": "search" - } - ], - "type": "visualization", - "updated_at": "2021-01-07T00:23:04.624Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "savedSearchRefName": "search_0", - "title": "Tag Cloud of Names", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"Tag Cloud of Names\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"customer_first_name.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true}}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "search:e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", - "index": ".kibana_1", - "source": { - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "search": { - "columns": [ - "order_date", - "category", - "currency", - "customer_id", - "order_id", - "day_of_week_i", - "products.created_on", - "sku" - ], - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "sort": [ - [ - "order_date", - "desc" - ] - ], - "title": "Ecommerce Data (copy)", - "version": 1 - }, - "type": "search", - "updated_at": "2021-05-03T18:39:30.751Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "dashboard:f7192e90-ac3c-11eb-8f24-bffe9ba4af2b", - "index": ".kibana_1", - "source": { - "dashboard": { - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" - }, - "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", - "panelsJSON": "[{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":9,\"y\":0,\"w\":24,\"h\":15,\"i\":\"914ac161-94d4-4d93-a287-c21fca46a974\"},\"panelIndex\":\"914ac161-94d4-4d93-a287-c21fca46a974\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_914ac161-94d4-4d93-a287-c21fca46a974\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":15,\"w\":24,\"h\":15,\"i\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\"},\"panelIndex\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c4cec7d1-97e3-4101-adc4-c3f15102511c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\"},\"panelIndex\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_09f7de68-0d07-4661-8fda-73ea8b577ac7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":45,\"w\":24,\"h\":15,\"i\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},\"panelIndex\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\"},\"panelIndex\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_37764cf9-3c89-454a-bd7e-ae4c242dc624\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":75,\"w\":24,\"h\":15,\"i\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},\"panelIndex\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":90,\"w\":24,\"h\":15,\"i\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},\"panelIndex\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":105,\"w\":24,\"h\":15,\"i\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\"},\"panelIndex\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_eee160de-5777-40c8-9c2c-e75f64bf208a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},\"panelIndex\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":135,\"w\":24,\"h\":15,\"i\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\"},\"panelIndex\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2e72acbf-7ade-451e-a5e4-7414f12facf2\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":150,\"w\":24,\"h\":15,\"i\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\"},\"panelIndex\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4119e9b0-5d03-482d-9356-89bb62b6a851\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":165,\"w\":24,\"h\":15,\"i\":\"42b4a37c-8b04-4510-9f27-831355221b65\"},\"panelIndex\":\"42b4a37c-8b04-4510-9f27-831355221b65\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_42b4a37c-8b04-4510-9f27-831355221b65\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":14,\"y\":180,\"w\":24,\"h\":15,\"i\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},\"panelIndex\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":195,\"w\":24,\"h\":15,\"i\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},\"panelIndex\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":210,\"w\":24,\"h\":15,\"i\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},\"panelIndex\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":225,\"w\":24,\"h\":15,\"i\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\"},\"panelIndex\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_69141f9b-5c23-409d-9c96-7f94c243f79e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":240,\"w\":24,\"h\":15,\"i\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\"},\"panelIndex\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6feeec2c-34ab-4844-8445-e417c8e0595b\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":255,\"w\":24,\"h\":15,\"i\":\"985d9dc1-de44-4803-afad-f1d497d050a1\"},\"panelIndex\":\"985d9dc1-de44-4803-afad-f1d497d050a1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_985d9dc1-de44-4803-afad-f1d497d050a1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":270,\"w\":24,\"h\":15,\"i\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},\"panelIndex\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":285,\"w\":24,\"h\":15,\"i\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\"},\"panelIndex\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6b0768b1-0cd2-47f0-a639-b369e7318d44\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":300,\"w\":24,\"h\":15,\"i\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\"},\"panelIndex\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_c9cc2835-06a8-4448-b703-2d41a6692feb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":315,\"w\":24,\"h\":15,\"i\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},\"panelIndex\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":330,\"w\":24,\"h\":15,\"i\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\"},\"panelIndex\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_ee92986a-adab-4d66-ad4e-a43a608f52f7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":345,\"w\":24,\"h\":15,\"i\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},\"panelIndex\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":360,\"w\":24,\"h\":15,\"i\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},\"panelIndex\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":375,\"w\":24,\"h\":15,\"i\":\"51122bae-427e-45a6-904e-6c821447cc46\"},\"panelIndex\":\"51122bae-427e-45a6-904e-6c821447cc46\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_51122bae-427e-45a6-904e-6c821447cc46\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":390,\"w\":24,\"h\":15,\"i\":\"4efab22c-1892-4013-8406-5e5d924a8a21\"},\"panelIndex\":\"4efab22c-1892-4013-8406-5e5d924a8a21\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4efab22c-1892-4013-8406-5e5d924a8a21\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":405,\"w\":24,\"h\":15,\"i\":\"4c3c1b29-100e-474c-8290-9470684ae407\"},\"panelIndex\":\"4c3c1b29-100e-474c-8290-9470684ae407\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4c3c1b29-100e-474c-8290-9470684ae407\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":420,\"w\":24,\"h\":15,\"i\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},\"panelIndex\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":435,\"w\":24,\"h\":15,\"i\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},\"panelIndex\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":450,\"w\":24,\"h\":15,\"i\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\"},\"panelIndex\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_13d9982e-2745-44b1-af94-fa4b9f6761a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":465,\"w\":24,\"h\":15,\"i\":\"efa18320-9650-4bfe-9418-ac29b7979f70\"},\"panelIndex\":\"efa18320-9650-4bfe-9418-ac29b7979f70\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_efa18320-9650-4bfe-9418-ac29b7979f70\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":480,\"w\":24,\"h\":15,\"i\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\"},\"panelIndex\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1f03bc70-0545-4a3a-bebc-ad477674b841\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":495,\"w\":24,\"h\":15,\"i\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},\"panelIndex\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":510,\"w\":24,\"h\":15,\"i\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},\"panelIndex\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":525,\"w\":24,\"h\":15,\"i\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\"},\"panelIndex\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b93cc5e1-084a-42d9-9958-a3f569573d43\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":540,\"w\":24,\"h\":15,\"i\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\"},\"panelIndex\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0b6c380f-3536-4f03-8dbd-95c53be69263\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":23,\"y\":555,\"w\":24,\"h\":15,\"i\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},\"panelIndex\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":570,\"w\":24,\"h\":15,\"i\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},\"panelIndex\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":1,\"y\":585,\"w\":24,\"h\":15,\"i\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},\"panelIndex\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":600,\"w\":24,\"h\":15,\"i\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\"},\"panelIndex\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_eb651411-ea02-4506-a674-f0125d0b2a4a\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":615,\"w\":48,\"h\":111,\"i\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},\"panelIndex\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":4,\"y\":852,\"w\":24,\"h\":15,\"i\":\"1201144d-5c9c-4015-89a3-0cb803405986\"},\"panelIndex\":\"1201144d-5c9c-4015-89a3-0cb803405986\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1201144d-5c9c-4015-89a3-0cb803405986\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":837,\"w\":24,\"h\":15,\"i\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\"},\"panelIndex\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_913c1c46-ded4-4e04-81ff-e683f725d3a5\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":867,\"w\":24,\"h\":15,\"i\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\"},\"panelIndex\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f49dfd93-ce95-4a65-b9ec-531f340da083\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":882,\"w\":24,\"h\":15,\"i\":\"0705993c-492c-4ce0-83e0-a481c90bd432\"},\"panelIndex\":\"0705993c-492c-4ce0-83e0-a481c90bd432\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0705993c-492c-4ce0-83e0-a481c90bd432\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":897,\"w\":24,\"h\":15,\"i\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\"},\"panelIndex\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_02de39d3-6839-4198-94e3-cc91f61d0c6e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":5,\"y\":912,\"w\":24,\"h\":15,\"i\":\"e6b958fa-931f-4358-94fc-07934419066d\"},\"panelIndex\":\"e6b958fa-931f-4358-94fc-07934419066d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6b958fa-931f-4358-94fc-07934419066d\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":927,\"w\":24,\"h\":15,\"i\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},\"panelIndex\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":942,\"w\":24,\"h\":15,\"i\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},\"panelIndex\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":726,\"w\":48,\"h\":111,\"i\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\"},\"panelIndex\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e985d8b0-4a76-46d0-af01-3edab5995b97\"}]", - "refreshInterval": { - "pause": true, - "value": 0 - }, - "timeFrom": "2019-06-01T03:59:54.350Z", - "timeRestore": true, - "timeTo": "2019-08-01T14:52:40.436Z", - "title": "Large Dashboard", - "version": 1 - }, - "references": [ - { - "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "name": "914ac161-94d4-4d93-a287-c21fca46a974:panel_914ac161-94d4-4d93-a287-c21fca46a974", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "c4cec7d1-97e3-4101-adc4-c3f15102511c:panel_c4cec7d1-97e3-4101-adc4-c3f15102511c", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "09f7de68-0d07-4661-8fda-73ea8b577ac7:panel_09f7de68-0d07-4661-8fda-73ea8b577ac7", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8:panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "37764cf9-3c89-454a-bd7e-ae4c242dc624:panel_37764cf9-3c89-454a-bd7e-ae4c242dc624", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "990422fd-a9cf-446f-ba2f-ea9178a7b2e0:panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "0cdc13ec-2775-4da9-9a47-1e833bb807eb:panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "eee160de-5777-40c8-9c2c-e75f64bf208a:panel_eee160de-5777-40c8-9c2c-e75f64bf208a", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb:panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "2e72acbf-7ade-451e-a5e4-7414f12facf2:panel_2e72acbf-7ade-451e-a5e4-7414f12facf2", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "4119e9b0-5d03-482d-9356-89bb62b6a851:panel_4119e9b0-5d03-482d-9356-89bb62b6a851", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "42b4a37c-8b04-4510-9f27-831355221b65:panel_42b4a37c-8b04-4510-9f27-831355221b65", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "dc676050-d752-4c3e-a1ae-73ef2f1bcdc6:panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "6602e0e0-9e66-4e0e-90c1-f66b9c3d2340:panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "c9c65725-9b4d-4343-93db-7efa4a7a2d60:panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "69141f9b-5c23-409d-9c96-7f94c243f79e:panel_69141f9b-5c23-409d-9c96-7f94c243f79e", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "6feeec2c-34ab-4844-8445-e417c8e0595b:panel_6feeec2c-34ab-4844-8445-e417c8e0595b", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "985d9dc1-de44-4803-afad-f1d497d050a1:panel_985d9dc1-de44-4803-afad-f1d497d050a1", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0:panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "6b0768b1-0cd2-47f0-a639-b369e7318d44:panel_6b0768b1-0cd2-47f0-a639-b369e7318d44", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "c9cc2835-06a8-4448-b703-2d41a6692feb:panel_c9cc2835-06a8-4448-b703-2d41a6692feb", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "af2a55b1-8b3d-478a-96b1-72e4f12585e4:panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "ee92986a-adab-4d66-ad4e-a43a608f52f7:panel_ee92986a-adab-4d66-ad4e-a43a608f52f7", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "3b4e1fd0-2acb-444a-b478-42d7bd10b96c:panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "04d7056d-88a4-4b00-b8f4-33f79f1b6f7a:panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "51122bae-427e-45a6-904e-6c821447cc46:panel_51122bae-427e-45a6-904e-6c821447cc46", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "4efab22c-1892-4013-8406-5e5d924a8a21:panel_4efab22c-1892-4013-8406-5e5d924a8a21", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "4c3c1b29-100e-474c-8290-9470684ae407:panel_4c3c1b29-100e-474c-8290-9470684ae407", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "b4501df0-d759-4513-9e87-5dd8eefe4a4f:panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6:panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "13d9982e-2745-44b1-af94-fa4b9f6761a9:panel_13d9982e-2745-44b1-af94-fa4b9f6761a9", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "efa18320-9650-4bfe-9418-ac29b7979f70:panel_efa18320-9650-4bfe-9418-ac29b7979f70", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "1f03bc70-0545-4a3a-bebc-ad477674b841:panel_1f03bc70-0545-4a3a-bebc-ad477674b841", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "d766ce3a-9ec5-4ead-8698-6a2e66e729bb:panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "de93deb0-6c16-45ae-8fae-de0b2e1c4ae0:panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "b93cc5e1-084a-42d9-9958-a3f569573d43:panel_b93cc5e1-084a-42d9-9958-a3f569573d43", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "0b6c380f-3536-4f03-8dbd-95c53be69263:panel_0b6c380f-3536-4f03-8dbd-95c53be69263", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "5c68b67a-ac42-48b8-85de-2409aaa0cdc6:panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "098a69b8-c9a0-40c8-8703-62838e0ec4a9:panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883:panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "eb651411-ea02-4506-a674-f0125d0b2a4a:panel_eb651411-ea02-4506-a674-f0125d0b2a4a", - "type": "visualization" - }, - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "8ec9b67a-5d08-4006-bccc-a7341b88bb63:panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63", - "type": "search" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "1201144d-5c9c-4015-89a3-0cb803405986:panel_1201144d-5c9c-4015-89a3-0cb803405986", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "913c1c46-ded4-4e04-81ff-e683f725d3a5:panel_913c1c46-ded4-4e04-81ff-e683f725d3a5", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "f49dfd93-ce95-4a65-b9ec-531f340da083:panel_f49dfd93-ce95-4a65-b9ec-531f340da083", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "0705993c-492c-4ce0-83e0-a481c90bd432:panel_0705993c-492c-4ce0-83e0-a481c90bd432", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "02de39d3-6839-4198-94e3-cc91f61d0c6e:panel_02de39d3-6839-4198-94e3-cc91f61d0c6e", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "e6b958fa-931f-4358-94fc-07934419066d:panel_e6b958fa-931f-4358-94fc-07934419066d", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "e6d70fc7-1bdc-4743-9a15-615dff91a5c1:panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa:panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa", - "type": "visualization" - }, - { - "id": "e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", - "name": "e985d8b0-4a76-46d0-af01-3edab5995b97:panel_e985d8b0-4a76-46d0-af01-3edab5995b97", - "type": "search" - } - ], - "type": "dashboard", - "updated_at": "2021-05-03T18:39:45.983Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json deleted file mode 100644 index 9170d8e00a77d..0000000000000 --- a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json +++ /dev/null @@ -1,2679 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "49eb3350984bd2a162914d3776e70cfb", - "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "0b7746a97518ec67b787d141886ad3c1", - "cases-comments": "8a50736330e953bca91747723a319593", - "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", - "cases-connector-mappings": "6bc7e49411d38be4969dc6aa8bd43776", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "dashboard": "40554caf09725935e2c02e02563a2d07", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "epm-packages": "0cbbb16506734d341a96aaed65ec6413", - "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", - "exception-list": "67f055ab8c10abd7b2ebfd969b836788", - "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", - "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", - "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", - "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", - "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", - "index-pattern": "45915a1ad866812242df474eb0479052", - "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", - "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", - "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", - "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", - "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", - "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "52346cfec69ff7b47d5f0c12361a2797", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "9134b47593116d7953f6adba096fc463", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", - "ml-job": "3bb64c31915acf93fc724af137a0891b", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "959dde12a55b3118eab009d8b2b72ad6", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "security-solution-signals-migration": "72761fd374ca11122ac8025a92b84fca", - "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "tag": "83d55da58f6530f7055415717ec06474", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "agent_actions": { - "dynamic": "false", - "type": "object" - }, - "agent_configs": { - "dynamic": "false", - "type": "object" - }, - "agent_events": { - "dynamic": "false", - "type": "object" - }, - "agents": { - "dynamic": "false", - "type": "object" - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "executionStatus": { - "properties": { - "error": { - "properties": { - "message": { - "type": "keyword" - }, - "reason": { - "type": "keyword" - } - } - }, - "lastExecutionDate": { - "type": "date" - }, - "status": { - "type": "keyword" - } - } - }, - "meta": { - "properties": { - "versionApiKeyLastmodified": { - "type": "keyword" - } - } - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "notifyWhen": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedAt": { - "type": "date" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "api_key_pending_invalidation": { - "properties": { - "apiKeyId": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "dynamic": "false", - "type": "object" - }, - "apm-telemetry": { - "dynamic": "false", - "type": "object" - }, - "app_search_telemetry": { - "dynamic": "false", - "type": "object" - }, - "application_usage_daily": { - "dynamic": "false", - "properties": { - "timestamp": { - "type": "date" - } - } - }, - "application_usage_totals": { - "dynamic": "false", - "type": "object" - }, - "application_usage_transactional": { - "dynamic": "false", - "type": "object" - }, - "background-session": { - "properties": { - "appId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "expires": { - "type": "date" - }, - "idMapping": { - "enabled": false, - "type": "object" - }, - "initialState": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "keyword" - }, - "restoreState": { - "enabled": false, - "type": "object" - }, - "sessionId": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "urlGeneratorId": { - "type": "keyword" - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad-template": { - "dynamic": "false", - "properties": { - "help": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "tags": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "template_key": { - "type": "keyword" - } - } - }, - "cases": { - "properties": { - "closed_at": { - "type": "date" - }, - "closed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "connector": { - "properties": { - "fields": { - "properties": { - "key": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "id": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "description": { - "type": "text" - }, - "external_service": { - "properties": { - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "external_id": { - "type": "keyword" - }, - "external_title": { - "type": "text" - }, - "external_url": { - "type": "text" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "settings": { - "properties": { - "syncAlerts": { - "type": "boolean" - } - } - }, - "status": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-comments": { - "properties": { - "alertId": { - "type": "keyword" - }, - "comment": { - "type": "text" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "index": { - "type": "keyword" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-configure": { - "properties": { - "closure_type": { - "type": "keyword" - }, - "connector": { - "properties": { - "fields": { - "properties": { - "key": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "id": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-connector-mappings": { - "properties": { - "mappings": { - "properties": { - "action_type": { - "type": "keyword" - }, - "source": { - "type": "keyword" - }, - "target": { - "type": "keyword" - } - } - } - } - }, - "cases-user-actions": { - "properties": { - "action": { - "type": "keyword" - }, - "action_at": { - "type": "date" - }, - "action_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "action_field": { - "type": "keyword" - }, - "new_value": { - "type": "text" - }, - "old_value": { - "type": "text" - } - } - }, - "config": { - "dynamic": "false", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "core-usage-stats": { - "dynamic": "false", - "type": "object" - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "optionsJSON": { - "index": false, - "type": "text" - }, - "panelsJSON": { - "index": false, - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "pause": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "section": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "value": { - "doc_values": false, - "index": false, - "type": "integer" - } - } - }, - "timeFrom": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "timeRestore": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "timeTo": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "datasources": { - "dynamic": "false", - "type": "object" - }, - "endpoint:user-artifact": { - "properties": { - "body": { - "type": "binary" - }, - "compressionAlgorithm": { - "index": false, - "type": "keyword" - }, - "created": { - "index": false, - "type": "date" - }, - "decodedSha256": { - "index": false, - "type": "keyword" - }, - "decodedSize": { - "index": false, - "type": "long" - }, - "encodedSha256": { - "type": "keyword" - }, - "encodedSize": { - "index": false, - "type": "long" - }, - "encryptionAlgorithm": { - "index": false, - "type": "keyword" - }, - "identifier": { - "type": "keyword" - } - } - }, - "endpoint:user-artifact-manifest": { - "properties": { - "created": { - "index": false, - "type": "date" - }, - "schemaVersion": { - "type": "keyword" - }, - "semanticVersion": { - "index": false, - "type": "keyword" - }, - "artifacts": { - "type": "nested", - "properties": { - "policyId": { - "type": "keyword", - "index": false - }, - "artifactId": { - "type": "keyword", - "index": false - } - } - } - } - }, - "enrollment_api_keys": { - "dynamic": "false", - "type": "object" - }, - "enterprise_search_telemetry": { - "dynamic": "false", - "type": "object" - }, - "epm-package": { - "dynamic": "false", - "type": "object" - }, - "epm-packages": { - "properties": { - "es_index_patterns": { - "enabled": false, - "type": "object" - }, - "install_source": { - "type": "keyword" - }, - "install_started_at": { - "type": "date" - }, - "install_status": { - "type": "keyword" - }, - "install_version": { - "type": "keyword" - }, - "installed_es": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "installed_kibana": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "internal": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "package_assets": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "removable": { - "type": "boolean" - }, - "version": { - "type": "keyword" - } - } - }, - "epm-packages-assets": { - "properties": { - "asset_path": { - "type": "keyword" - }, - "data_base64": { - "type": "binary" - }, - "data_utf8": { - "index": false, - "type": "text" - }, - "install_source": { - "type": "keyword" - }, - "media_type": { - "type": "keyword" - }, - "package_name": { - "type": "keyword" - }, - "package_version": { - "type": "keyword" - } - } - }, - "exception-list": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os_types": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "exception-list-agnostic": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os_types": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "fleet-agent-actions": { - "properties": { - "ack_data": { - "type": "text" - }, - "agent_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "data": { - "type": "binary" - }, - "policy_id": { - "type": "keyword" - }, - "policy_revision": { - "type": "integer" - }, - "sent_at": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "fleet-agent-events": { - "properties": { - "action_id": { - "type": "keyword" - }, - "agent_id": { - "type": "keyword" - }, - "data": { - "type": "text" - }, - "message": { - "type": "text" - }, - "payload": { - "type": "text" - }, - "policy_id": { - "type": "keyword" - }, - "stream_id": { - "type": "keyword" - }, - "subtype": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "fleet-agents": { - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "current_error_events": { - "index": false, - "type": "text" - }, - "default_api_key": { - "type": "binary" - }, - "default_api_key_id": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_checkin_status": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "type": "flattened" - }, - "packages": { - "type": "keyword" - }, - "policy_id": { - "type": "keyword" - }, - "policy_revision": { - "type": "integer" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "unenrolled_at": { - "type": "date" - }, - "unenrollment_started_at": { - "type": "date" - }, - "updated_at": { - "type": "date" - }, - "upgrade_started_at": { - "type": "date" - }, - "upgraded_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "flattened" - }, - "version": { - "type": "keyword" - } - } - }, - "fleet-enrollment-api-keys": { - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "binary" - }, - "api_key_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "policy_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "legacyIndexPatternRef": { - "index": false, - "type": "text" - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "dynamic": "false", - "properties": { - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "dynamic": "false", - "type": "object" - }, - "ingest-agent-policies": { - "properties": { - "description": { - "type": "text" - }, - "is_default": { - "type": "boolean" - }, - "monitoring_enabled": { - "index": false, - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "package_policies": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "status": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "ingest-outputs": { - "properties": { - "ca_sha256": { - "index": false, - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "config_yaml": { - "type": "text" - }, - "fleet_enroll_password": { - "type": "binary" - }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "ingest-package-policies": { - "properties": { - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "enabled": { - "type": "boolean" - }, - "inputs": { - "enabled": false, - "properties": { - "compiled_input": { - "type": "flattened" - }, - "config": { - "type": "flattened" - }, - "enabled": { - "type": "boolean" - }, - "streams": { - "properties": { - "compiled_stream": { - "type": "flattened" - }, - "config": { - "type": "flattened" - }, - "data_stream": { - "properties": { - "dataset": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "type": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "output_id": { - "type": "keyword" - }, - "package": { - "properties": { - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "policy_id": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "ingest_manager_settings": { - "properties": { - "agent_auto_upgrade": { - "type": "keyword" - }, - "has_seen_add_data_notice": { - "index": false, - "type": "boolean" - }, - "kibana_ca_sha256": { - "type": "keyword" - }, - "kibana_urls": { - "type": "keyword" - }, - "package_auto_upgrade": { - "type": "keyword" - } - } - }, - "inventory-view": { - "dynamic": "false", - "type": "object" - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "description": { - "type": "text" - }, - "expression": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": "false", - "type": "object" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "enabled": false, - "type": "object" - }, - "metrics-explorer-view": { - "dynamic": "false", - "type": "object" - }, - "ml-job": { - "properties": { - "datafeed_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "job_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "monitoring-telemetry": { - "properties": { - "reportedClusterUuids": { - "type": "keyword" - } - } - }, - "namespace": { - "type": "keyword" - }, - "namespaces": { - "type": "keyword" - }, - "originId": { - "type": "keyword" - }, - "outputs": { - "dynamic": "false", - "type": "object" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "description": { - "type": "text" - }, - "grid": { - "enabled": false, - "type": "object" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "sort": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "search-telemetry": { - "dynamic": "false", - "type": "object" - }, - "security-solution-signals-migration": { - "properties": { - "created": { - "index": false, - "type": "date" - }, - "createdBy": { - "index": false, - "type": "text" - }, - "destinationIndex": { - "index": false, - "type": "keyword" - }, - "error": { - "index": false, - "type": "text" - }, - "sourceIndex": { - "type": "keyword" - }, - "status": { - "index": false, - "type": "keyword" - }, - "taskId": { - "index": false, - "type": "keyword" - }, - "updated": { - "index": false, - "type": "date" - }, - "updatedBy": { - "index": false, - "type": "text" - }, - "version": { - "type": "long" - } - } - }, - "server": { - "dynamic": "false", - "type": "object" - }, - "siem-detection-engine-rule-actions": { - "properties": { - "actions": { - "properties": { - "action_type_id": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alertThrottle": { - "type": "keyword" - }, - "ruleAlertId": { - "type": "keyword" - }, - "ruleThrottle": { - "type": "keyword" - } - } - }, - "siem-detection-engine-rule-status": { - "properties": { - "alertId": { - "type": "keyword" - }, - "bulkCreateTimeDurations": { - "type": "float" - }, - "gap": { - "type": "text" - }, - "lastFailureAt": { - "type": "date" - }, - "lastFailureMessage": { - "type": "text" - }, - "lastLookBackDate": { - "type": "date" - }, - "lastSuccessAt": { - "type": "date" - }, - "lastSuccessMessage": { - "type": "text" - }, - "searchAfterTimeDurations": { - "type": "float" - }, - "status": { - "type": "keyword" - }, - "statusDate": { - "type": "date" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "eventType": { - "type": "keyword" - }, - "excludedRowRendererIds": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "indexNames": { - "type": "text" - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "status": { - "type": "keyword" - }, - "templateTimelineId": { - "type": "text" - }, - "templateTimelineVersion": { - "type": "integer" - }, - "timelineType": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "spaces-usage-stats": { - "dynamic": "false", - "type": "object" - }, - "tag": { - "properties": { - "color": { - "type": "text" - }, - "description": { - "type": "text" - }, - "name": { - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "allowChangingOptInStatus": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword" - }, - "reportFailureCount": { - "type": "integer" - }, - "reportFailureVersion": { - "type": "keyword" - }, - "sendUsageFrom": { - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-counter": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "properties": { - "errorMessage": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "indexName": { - "type": "keyword" - }, - "lastCompletedStep": { - "type": "long" - }, - "locked": { - "type": "date" - }, - "newIndexName": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexOptions": { - "properties": { - "openAndClose": { - "type": "boolean" - }, - "queueSettings": { - "properties": { - "queuedAt": { - "type": "long" - }, - "startedAt": { - "type": "long" - } - } - } - } - }, - "reindexTaskId": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexTaskPercComplete": { - "type": "float" - }, - "runningReindexCount": { - "type": "integer" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "uptime-dynamic-settings": { - "dynamic": "false", - "type": "object" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "savedSearchRefName": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "index": false, - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "index": false, - "type": "text" - } - } - }, - "workplace_search_telemetry": { - "dynamic": "false", - "type": "object" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json b/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json new file mode 100644 index 0000000000000..568b2e17a9332 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json @@ -0,0 +1,678 @@ +{ + "attributes": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"category\"}}},{\"name\":\"currency\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_birth_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_first_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_first_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_first_name\"}}},{\"name\":\"customer_full_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_full_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_full_name\"}}},{\"name\":\"customer_gender\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_last_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_last_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_last_name\"}}},{\"name\":\"customer_phone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week_i\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"manufacturer\"}}},{\"name\":\"order_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"order_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products._id\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products._id.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products._id\"}}},{\"name\":\"products.base_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.base_unit_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.category\"}}},{\"name\":\"products.created_on\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_percentage\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.manufacturer\"}}},{\"name\":\"products.min_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.product_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.product_name\"}}},{\"name\":\"products.quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.tax_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxful_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxless_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.unit_discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxful_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxless_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_unique_products\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "order_date", + "title": "ecommerce" + }, + "coreMigrationVersion": "8.0.0", + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-12-11T23:24:13.381Z", + "version": "WzE3LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "e-commerce area chart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"title\":\"e-commerce area chart\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-08T23:24:05.971Z", + "version": "WzIwLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" + }, + "title": "Українська", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"metric\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"Українська\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-10T00:34:44.700Z", + "version": "WzIzLDJd" +} + +{ + "attributes": { + "columns": [ + "order_date", + "category", + "currency", + "customer_id", + "order_id", + "day_of_week_i", + "products.created_on", + "sku" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "order_date", + "desc" + ] + ], + "title": "Ecommerce Data", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "updated_at": "2019-12-11T23:24:28.540Z", + "version": "WzE4LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Tag Cloud of Names", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Tag Cloud of Names\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"customer_first_name.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"}}}" + }, + "coreMigrationVersion": "8.0.0", + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2021-01-07T00:23:04.624Z", + "version": "WzI3LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "e-commerce pie chart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"title\":\"e-commerce pie chart\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-08T23:24:42.460Z", + "version": "WzIxLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "Tiểu thuyết", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"markdown\",\"aggs\":[],\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Tiểu thuyết là một thể loại văn xuôi có hư cấu, thông qua nhân vật, hoàn cảnh, sự việc để phản ánh bức tranh xã hội rộng lớn và những vấn đề của cuộc sống con người, biểu hiện tính chất tường thuật, tính chất kể chuyện bằng ngôn ngữ văn xuôi theo những chủ đề xác định.\\n\\nTrong một cách hiểu khác, nhận định của Belinski: \\\"tiểu thuyết là sử thi của đời tư\\\" chỉ ra khái quát nhất về một dạng thức tự sự, trong đó sự trần thuật tập trung vào số phận của một cá nhân trong quá trình hình thành và phát triển của nó. Sự trần thuật ở đây được khai triển trong không gian và thời gian nghệ thuật đến mức đủ để truyền đạt cơ cấu của nhân cách[1].\\n\\n\\n[1]^ Mục từ Tiểu thuyết trong cuốn 150 thuật ngữ văn học, Lại Nguyên Ân biên soạn, Nhà xuất bản Đại học Quốc gia Hà Nội, in lần thứ 2 có sửa đổi bổ sung. H. 2003. Trang 326.\"},\"title\":\"Tiểu thuyết\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [], + "type": "visualization", + "updated_at": "2020-04-10T00:36:17.053Z", + "version": "WzI0LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" + }, + "title": "게이지", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"gauge\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"rgba(105,112,125,0.2)\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"rgba(105,112,125,0.2)\",\"bgColor\":true,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"게이지\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-10T00:33:44.909Z", + "version": "WzIyLDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":35,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":23,\"w\":16,\"h\":12,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":16,\"y\":23,\"w\":12,\"h\":12,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":28,\"y\":23,\"w\":20,\"h\":12,\"i\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\"},\"panelIndex\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-03-23T03:06:17.785Z", + "timeRestore": true, + "timeTo": "2019-10-04T02:33:16.708Z", + "title": "Ecom Dashboard", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "6c263e00-1c6d-11ea-a100-8589bb9d7c6b", + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "panel_2", + "type": "search" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_5", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "panel_6", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2021-01-07T00:22:16.102Z", + "version": "WzI2LDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":true,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":33,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":41,\"w\":11,\"h\":10,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":11,\"y\":41,\"w\":5,\"h\":10,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-06-26T06:20:28.066Z", + "timeRestore": true, + "timeTo": "2019-06-26T07:27:58.573Z", + "title": "Ecom Dashboard Hidden Panel Titles", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "constructed-sample-saved-object-id", + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "panel_2", + "type": "search" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_5", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-04-10T00:37:48.462Z", + "version": "WzE5LDJd" +} + +{ + "attributes": { + "columns": [ + "order_date", + "category", + "currency", + "customer_id", + "order_id", + "day_of_week_i", + "products.created_on", + "sku" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "order_date", + "desc" + ] + ], + "title": "Ecommerce Data (copy)", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "updated_at": "2021-05-03T18:39:30.751Z", + "version": "WzI4LDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":9,\"y\":0,\"w\":24,\"h\":15,\"i\":\"914ac161-94d4-4d93-a287-c21fca46a974\"},\"panelIndex\":\"914ac161-94d4-4d93-a287-c21fca46a974\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_914ac161-94d4-4d93-a287-c21fca46a974\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":15,\"w\":24,\"h\":15,\"i\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\"},\"panelIndex\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c4cec7d1-97e3-4101-adc4-c3f15102511c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\"},\"panelIndex\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_09f7de68-0d07-4661-8fda-73ea8b577ac7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":45,\"w\":24,\"h\":15,\"i\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},\"panelIndex\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\"},\"panelIndex\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_37764cf9-3c89-454a-bd7e-ae4c242dc624\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":75,\"w\":24,\"h\":15,\"i\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},\"panelIndex\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":90,\"w\":24,\"h\":15,\"i\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},\"panelIndex\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":105,\"w\":24,\"h\":15,\"i\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\"},\"panelIndex\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_eee160de-5777-40c8-9c2c-e75f64bf208a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},\"panelIndex\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":135,\"w\":24,\"h\":15,\"i\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\"},\"panelIndex\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2e72acbf-7ade-451e-a5e4-7414f12facf2\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":150,\"w\":24,\"h\":15,\"i\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\"},\"panelIndex\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4119e9b0-5d03-482d-9356-89bb62b6a851\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":165,\"w\":24,\"h\":15,\"i\":\"42b4a37c-8b04-4510-9f27-831355221b65\"},\"panelIndex\":\"42b4a37c-8b04-4510-9f27-831355221b65\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_42b4a37c-8b04-4510-9f27-831355221b65\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":14,\"y\":180,\"w\":24,\"h\":15,\"i\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},\"panelIndex\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":195,\"w\":24,\"h\":15,\"i\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},\"panelIndex\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":210,\"w\":24,\"h\":15,\"i\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},\"panelIndex\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":225,\"w\":24,\"h\":15,\"i\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\"},\"panelIndex\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_69141f9b-5c23-409d-9c96-7f94c243f79e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":240,\"w\":24,\"h\":15,\"i\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\"},\"panelIndex\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6feeec2c-34ab-4844-8445-e417c8e0595b\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":255,\"w\":24,\"h\":15,\"i\":\"985d9dc1-de44-4803-afad-f1d497d050a1\"},\"panelIndex\":\"985d9dc1-de44-4803-afad-f1d497d050a1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_985d9dc1-de44-4803-afad-f1d497d050a1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":270,\"w\":24,\"h\":15,\"i\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},\"panelIndex\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":285,\"w\":24,\"h\":15,\"i\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\"},\"panelIndex\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6b0768b1-0cd2-47f0-a639-b369e7318d44\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":300,\"w\":24,\"h\":15,\"i\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\"},\"panelIndex\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_c9cc2835-06a8-4448-b703-2d41a6692feb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":315,\"w\":24,\"h\":15,\"i\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},\"panelIndex\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":330,\"w\":24,\"h\":15,\"i\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\"},\"panelIndex\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_ee92986a-adab-4d66-ad4e-a43a608f52f7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":345,\"w\":24,\"h\":15,\"i\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},\"panelIndex\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":360,\"w\":24,\"h\":15,\"i\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},\"panelIndex\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":375,\"w\":24,\"h\":15,\"i\":\"51122bae-427e-45a6-904e-6c821447cc46\"},\"panelIndex\":\"51122bae-427e-45a6-904e-6c821447cc46\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_51122bae-427e-45a6-904e-6c821447cc46\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":390,\"w\":24,\"h\":15,\"i\":\"4efab22c-1892-4013-8406-5e5d924a8a21\"},\"panelIndex\":\"4efab22c-1892-4013-8406-5e5d924a8a21\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4efab22c-1892-4013-8406-5e5d924a8a21\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":405,\"w\":24,\"h\":15,\"i\":\"4c3c1b29-100e-474c-8290-9470684ae407\"},\"panelIndex\":\"4c3c1b29-100e-474c-8290-9470684ae407\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4c3c1b29-100e-474c-8290-9470684ae407\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":420,\"w\":24,\"h\":15,\"i\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},\"panelIndex\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":435,\"w\":24,\"h\":15,\"i\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},\"panelIndex\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":450,\"w\":24,\"h\":15,\"i\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\"},\"panelIndex\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_13d9982e-2745-44b1-af94-fa4b9f6761a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":465,\"w\":24,\"h\":15,\"i\":\"efa18320-9650-4bfe-9418-ac29b7979f70\"},\"panelIndex\":\"efa18320-9650-4bfe-9418-ac29b7979f70\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_efa18320-9650-4bfe-9418-ac29b7979f70\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":480,\"w\":24,\"h\":15,\"i\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\"},\"panelIndex\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1f03bc70-0545-4a3a-bebc-ad477674b841\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":495,\"w\":24,\"h\":15,\"i\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},\"panelIndex\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":510,\"w\":24,\"h\":15,\"i\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},\"panelIndex\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":525,\"w\":24,\"h\":15,\"i\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\"},\"panelIndex\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b93cc5e1-084a-42d9-9958-a3f569573d43\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":540,\"w\":24,\"h\":15,\"i\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\"},\"panelIndex\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0b6c380f-3536-4f03-8dbd-95c53be69263\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":23,\"y\":555,\"w\":24,\"h\":15,\"i\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},\"panelIndex\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":570,\"w\":24,\"h\":15,\"i\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},\"panelIndex\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":1,\"y\":585,\"w\":24,\"h\":15,\"i\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},\"panelIndex\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":600,\"w\":24,\"h\":15,\"i\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\"},\"panelIndex\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_eb651411-ea02-4506-a674-f0125d0b2a4a\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":615,\"w\":48,\"h\":111,\"i\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},\"panelIndex\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":4,\"y\":852,\"w\":24,\"h\":15,\"i\":\"1201144d-5c9c-4015-89a3-0cb803405986\"},\"panelIndex\":\"1201144d-5c9c-4015-89a3-0cb803405986\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1201144d-5c9c-4015-89a3-0cb803405986\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":837,\"w\":24,\"h\":15,\"i\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\"},\"panelIndex\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_913c1c46-ded4-4e04-81ff-e683f725d3a5\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":867,\"w\":24,\"h\":15,\"i\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\"},\"panelIndex\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f49dfd93-ce95-4a65-b9ec-531f340da083\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":882,\"w\":24,\"h\":15,\"i\":\"0705993c-492c-4ce0-83e0-a481c90bd432\"},\"panelIndex\":\"0705993c-492c-4ce0-83e0-a481c90bd432\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0705993c-492c-4ce0-83e0-a481c90bd432\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":897,\"w\":24,\"h\":15,\"i\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\"},\"panelIndex\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_02de39d3-6839-4198-94e3-cc91f61d0c6e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":5,\"y\":912,\"w\":24,\"h\":15,\"i\":\"e6b958fa-931f-4358-94fc-07934419066d\"},\"panelIndex\":\"e6b958fa-931f-4358-94fc-07934419066d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6b958fa-931f-4358-94fc-07934419066d\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":927,\"w\":24,\"h\":15,\"i\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},\"panelIndex\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":942,\"w\":24,\"h\":15,\"i\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},\"panelIndex\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":726,\"w\":48,\"h\":111,\"i\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\"},\"panelIndex\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e985d8b0-4a76-46d0-af01-3edab5995b97\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-06-01T03:59:54.350Z", + "timeRestore": true, + "timeTo": "2019-08-01T14:52:40.436Z", + "title": "Large Dashboard", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "f7192e90-ac3c-11eb-8f24-bffe9ba4af2b", + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "914ac161-94d4-4d93-a287-c21fca46a974:panel_914ac161-94d4-4d93-a287-c21fca46a974", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "c4cec7d1-97e3-4101-adc4-c3f15102511c:panel_c4cec7d1-97e3-4101-adc4-c3f15102511c", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "09f7de68-0d07-4661-8fda-73ea8b577ac7:panel_09f7de68-0d07-4661-8fda-73ea8b577ac7", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8:panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "37764cf9-3c89-454a-bd7e-ae4c242dc624:panel_37764cf9-3c89-454a-bd7e-ae4c242dc624", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "990422fd-a9cf-446f-ba2f-ea9178a7b2e0:panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "0cdc13ec-2775-4da9-9a47-1e833bb807eb:panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "eee160de-5777-40c8-9c2c-e75f64bf208a:panel_eee160de-5777-40c8-9c2c-e75f64bf208a", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb:panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "2e72acbf-7ade-451e-a5e4-7414f12facf2:panel_2e72acbf-7ade-451e-a5e4-7414f12facf2", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "4119e9b0-5d03-482d-9356-89bb62b6a851:panel_4119e9b0-5d03-482d-9356-89bb62b6a851", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "42b4a37c-8b04-4510-9f27-831355221b65:panel_42b4a37c-8b04-4510-9f27-831355221b65", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "dc676050-d752-4c3e-a1ae-73ef2f1bcdc6:panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "6602e0e0-9e66-4e0e-90c1-f66b9c3d2340:panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "c9c65725-9b4d-4343-93db-7efa4a7a2d60:panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "69141f9b-5c23-409d-9c96-7f94c243f79e:panel_69141f9b-5c23-409d-9c96-7f94c243f79e", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "6feeec2c-34ab-4844-8445-e417c8e0595b:panel_6feeec2c-34ab-4844-8445-e417c8e0595b", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "985d9dc1-de44-4803-afad-f1d497d050a1:panel_985d9dc1-de44-4803-afad-f1d497d050a1", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0:panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "6b0768b1-0cd2-47f0-a639-b369e7318d44:panel_6b0768b1-0cd2-47f0-a639-b369e7318d44", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "c9cc2835-06a8-4448-b703-2d41a6692feb:panel_c9cc2835-06a8-4448-b703-2d41a6692feb", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "af2a55b1-8b3d-478a-96b1-72e4f12585e4:panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "ee92986a-adab-4d66-ad4e-a43a608f52f7:panel_ee92986a-adab-4d66-ad4e-a43a608f52f7", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "3b4e1fd0-2acb-444a-b478-42d7bd10b96c:panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "04d7056d-88a4-4b00-b8f4-33f79f1b6f7a:panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "51122bae-427e-45a6-904e-6c821447cc46:panel_51122bae-427e-45a6-904e-6c821447cc46", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "4efab22c-1892-4013-8406-5e5d924a8a21:panel_4efab22c-1892-4013-8406-5e5d924a8a21", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "4c3c1b29-100e-474c-8290-9470684ae407:panel_4c3c1b29-100e-474c-8290-9470684ae407", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "b4501df0-d759-4513-9e87-5dd8eefe4a4f:panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6:panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "13d9982e-2745-44b1-af94-fa4b9f6761a9:panel_13d9982e-2745-44b1-af94-fa4b9f6761a9", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "efa18320-9650-4bfe-9418-ac29b7979f70:panel_efa18320-9650-4bfe-9418-ac29b7979f70", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "1f03bc70-0545-4a3a-bebc-ad477674b841:panel_1f03bc70-0545-4a3a-bebc-ad477674b841", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "d766ce3a-9ec5-4ead-8698-6a2e66e729bb:panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "de93deb0-6c16-45ae-8fae-de0b2e1c4ae0:panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "b93cc5e1-084a-42d9-9958-a3f569573d43:panel_b93cc5e1-084a-42d9-9958-a3f569573d43", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "0b6c380f-3536-4f03-8dbd-95c53be69263:panel_0b6c380f-3536-4f03-8dbd-95c53be69263", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "5c68b67a-ac42-48b8-85de-2409aaa0cdc6:panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "098a69b8-c9a0-40c8-8703-62838e0ec4a9:panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883:panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "eb651411-ea02-4506-a674-f0125d0b2a4a:panel_eb651411-ea02-4506-a674-f0125d0b2a4a", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "8ec9b67a-5d08-4006-bccc-a7341b88bb63:panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63", + "type": "search" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "1201144d-5c9c-4015-89a3-0cb803405986:panel_1201144d-5c9c-4015-89a3-0cb803405986", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "913c1c46-ded4-4e04-81ff-e683f725d3a5:panel_913c1c46-ded4-4e04-81ff-e683f725d3a5", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "f49dfd93-ce95-4a65-b9ec-531f340da083:panel_f49dfd93-ce95-4a65-b9ec-531f340da083", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "0705993c-492c-4ce0-83e0-a481c90bd432:panel_0705993c-492c-4ce0-83e0-a481c90bd432", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "02de39d3-6839-4198-94e3-cc91f61d0c6e:panel_02de39d3-6839-4198-94e3-cc91f61d0c6e", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "e6b958fa-931f-4358-94fc-07934419066d:panel_e6b958fa-931f-4358-94fc-07934419066d", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "e6d70fc7-1bdc-4743-9a15-615dff91a5c1:panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa:panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa", + "type": "visualization" + }, + { + "id": "e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", + "name": "e985d8b0-4a76-46d0-af01-3edab5995b97:panel_e985d8b0-4a76-46d0-af01-3edab5995b97", + "type": "search" + } + ], + "type": "dashboard", + "updated_at": "2021-05-03T18:39:45.983Z", + "version": "WzI5LDJd" +} \ No newline at end of file diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index bfbf030b0887a..e45af4bd140b0 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -22,8 +22,10 @@ export function createScenarios({ getService }: Pick { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }; const teardownEcommerce = async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await deleteAllReports(); }; diff --git a/x-pack/test/reporting_functional/reporting_without_security/management.ts b/x-pack/test/reporting_functional/reporting_without_security/management.ts index 030c890c963b1..a97cb211b7c0e 100644 --- a/x-pack/test/reporting_functional/reporting_without_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_without_security/management.ts @@ -14,10 +14,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const PageObjects = getPageObjects(['common', 'reporting']); const log = getService('log'); const supertest = getService('supertestWithoutAuth'); - + const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const reportingApi = getService('reportingAPI'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; const postJobJSON = async ( apiPath: string, @@ -31,12 +32,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Polling for jobs', () => { beforeEach(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }); afterEach(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await reportingApi.deleteAllReports(); }); From 9a5efd88877b916cd0a6fabb38e5c8fa014ae8de Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 22 Jun 2021 19:26:31 -0700 Subject: [PATCH 089/118] Migrate Index Management to new solutions nav (#101548) (#103029) * Migrate index template and component template wizard pages to new nav. * Convert index templates and component templates pages to new nav. * Convert indices and data streams pages to new nav. * Add PageLoading component to es_ui_shared. * Refactor index table component tests. * Add missing error reporting to get all templates API route handler. # Conflicts: # x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts --- .../authorization/components/page_error.tsx | 38 +-- .../public/components/page_loading/index.ts | 9 + .../components/page_loading/page_loading.tsx | 22 ++ src/plugins/es_ui_shared/public/index.ts | 1 + .../__jest__/components/index_table.test.js | 229 ++++++++++++++---- .../component_template_list.test.ts | 4 +- .../component_template_list.tsx | 39 ++- .../component_template_list/error.tsx | 40 --- .../with_privileges.tsx | 10 +- .../component_template_clone.tsx | 9 +- .../component_template_create.tsx | 39 +-- .../component_template_edit.tsx | 85 +++---- .../components/component_templates/lib/api.ts | 3 +- .../component_templates/lib/request.ts | 1 + .../component_templates/shared_imports.ts | 4 +- .../public/application/components/index.ts | 2 - .../components/page_error/index.ts | 8 - .../page_error/page_error_forbidden.tsx | 30 --- .../components/section_loading.tsx | 24 -- .../template_form/template_form.tsx | 4 +- .../data_stream_detail_panel.tsx | 4 +- .../data_stream_list/data_stream_list.tsx | 22 +- .../sections/home/index_list/index_list.tsx | 3 +- .../index_list/index_table/index_table.js | 121 ++++----- .../template_details_content.tsx | 4 +- .../home/template_list/template_list.tsx | 160 ++++++------ .../template_clone/template_clone.tsx | 53 ++-- .../template_create/template_create.tsx | 54 ++--- .../sections/template_edit/template_edit.tsx | 136 +++++------ .../application/services/use_request.ts | 5 +- .../index_management/public/shared_imports.ts | 6 + .../api/templates/register_get_routes.ts | 56 +++-- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 34 files changed, 663 insertions(+), 566 deletions(-) create mode 100644 src/plugins/es_ui_shared/public/components/page_loading/index.ts create mode 100644 src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/page_error/index.ts delete mode 100644 x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/section_loading.tsx diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx index 0a27b4098681b..732aa35b05237 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx @@ -13,7 +13,7 @@ import { Error } from '../types'; interface Props { title: React.ReactNode; - error: Error; + error?: Error; actions?: JSX.Element; isCentered?: boolean; } @@ -32,30 +32,30 @@ export const PageError: React.FunctionComponent = ({ isCentered, ...rest }) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error; + const errorString = error?.error; + const cause = error?.cause; // wrapEsError() on the server adds a "cause" array + const message = error?.message; const errorContent = ( {title}} body={ - <> - {cause ? message || errorString :

    {message || errorString}

    } - {cause && ( - <> - -
      - {cause.map((causeMsg, i) => ( -
    • {causeMsg}
    • - ))} -
    - - )} - + error && ( + <> + {cause ? message || errorString :

    {message || errorString}

    } + {cause && ( + <> + +
      + {cause.map((causeMsg, i) => ( +
    • {causeMsg}
    • + ))} +
    + + )} + + ) } iconType="alert" actions={actions} diff --git a/src/plugins/es_ui_shared/public/components/page_loading/index.ts b/src/plugins/es_ui_shared/public/components/page_loading/index.ts new file mode 100644 index 0000000000000..3e7b93bb4e7c3 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/page_loading/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { PageLoading } from './page_loading'; diff --git a/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx new file mode 100644 index 0000000000000..2fb99208e58ac --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText, EuiPageContent } from '@elastic/eui'; + +export const PageLoading: React.FunctionComponent = ({ children }) => { + return ( + + } + body={{children}} + data-test-subj="sectionLoading" + /> + + ); +}; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 7b9013c043a0e..ef2e2daa25468 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -17,6 +17,7 @@ import * as XJson from './xjson'; export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor'; +export { PageLoading } from './components/page_loading'; export { SectionLoading } from './components/section_loading'; export { Frequency, CronEditor } from './components/cron_editor'; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 4ac94319d4711..463d0b30cad08 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -6,9 +6,12 @@ */ import React from 'react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; import axios from 'axios'; +import sinon from 'sinon'; +import { findTestSubject } from '@elastic/eui/lib/test'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { MemoryRouter } from 'react-router-dom'; /** * The below import is required to avoid a console error warn from brace package @@ -18,9 +21,9 @@ import { MemoryRouter } from 'react-router-dom'; */ import { mountWithIntl, stubWebWorker } from '@kbn/test/jest'; // eslint-disable-line no-unused-vars +import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; import { AppWithoutRouter } from '../../public/application/app'; import { AppContextProvider } from '../../public/application/app_context'; -import { Provider } from 'react-redux'; import { loadIndicesSuccess } from '../../public/application/store/actions'; import { breadcrumbService } from '../../public/application/services/breadcrumbs'; import { UiMetricService } from '../../public/application/services/ui_metric'; @@ -29,10 +32,7 @@ import { httpService } from '../../public/application/services/http'; import { setUiMetricService } from '../../public/application/services/api'; import { indexManagementStore } from '../../public/application/store'; import { setExtensionsService } from '../../public/application/store/selectors/extension_service'; -import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; import { ExtensionsService } from '../../public/services'; -import sinon from 'sinon'; -import { findTestSubject } from '@elastic/eui/lib/test'; /* eslint-disable @kbn/eslint/no-restricted-paths */ import { notificationServiceMock } from '../../../../../src/core/public/notifications/notifications_service.mock'; @@ -40,9 +40,9 @@ import { notificationServiceMock } from '../../../../../src/core/public/notifica const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); let server = null; - let store = null; const indices = []; + for (let i = 0; i < 105; i++) { const baseFake = { health: i % 2 === 0 ? 'green' : 'yellow', @@ -63,8 +63,12 @@ for (let i = 0; i < 105; i++) { name: `.admin${i}`, }); } + let component = null; +// Resolve outstanding API requests. See https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/ +const runAllPromises = () => new Promise(setImmediate); + const status = (rendered, row = 0) => { rendered.update(); return findTestSubject(rendered, 'indexTableCell-status') @@ -76,39 +80,54 @@ const status = (rendered, row = 0) => { const snapshot = (rendered) => { expect(rendered).toMatchSnapshot(); }; + const openMenuAndClickButton = (rendered, rowIndex, buttonIndex) => { + // Select a row. const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(rowIndex).simulate('change', { target: { checked: true } }); rendered.update(); + + // Click the bulk actions button to open the context menu. const actionButton = findTestSubject(rendered, 'indexActionsContextMenuButton'); actionButton.simulate('click'); rendered.update(); + + // Click an action in the context menu. const contextMenuButtons = findTestSubject(rendered, 'indexTableContextMenuButton'); contextMenuButtons.at(buttonIndex).simulate('click'); + rendered.update(); }; -const testEditor = (buttonIndex, rowIndex = 0) => { - const rendered = mountWithIntl(component); + +const testEditor = (rendered, buttonIndex, rowIndex = 0) => { openMenuAndClickButton(rendered, rowIndex, buttonIndex); rendered.update(); snapshot(findTestSubject(rendered, 'detailPanelTabSelected').text()); }; -const testAction = (buttonIndex, done, rowIndex = 0) => { - const rendered = mountWithIntl(component); - let count = 0; + +const testAction = (rendered, buttonIndex, rowIndex = 0) => { + // This is leaking some implementation details about how Redux works. Not sure exactly what's going on + // but it looks like we're aware of how many Redux actions are dispatched in response to user interaction, + // so we "time" our assertion based on how many Redux actions we observe. This is brittle because it + // depends upon how our UI is architected, which will affect how many actions are dispatched. + // Expect this to break when we rearchitect the UI. + let dispatchedActionsCount = 0; store.subscribe(() => { - if (count > 1) { + if (dispatchedActionsCount === 1) { + // Take snapshot of final state. snapshot(status(rendered, rowIndex)); - done(); } - count++; + dispatchedActionsCount++; }); - expect.assertions(2); + openMenuAndClickButton(rendered, rowIndex, buttonIndex); + // take snapshot of initial state. snapshot(status(rendered, rowIndex)); }; + const names = (rendered) => { return findTestSubject(rendered, 'indexTableIndexNameLink'); }; + const namesText = (rendered) => { return names(rendered).map((button) => button.text()); }; @@ -142,23 +161,28 @@ describe('index table', () => { ); + store.dispatch(loadIndicesSuccess({ indices })); server = sinon.fakeServer.create(); + server.respondWith(`${API_BASE_PATH}/indices`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(indices), ]); + server.respondWith([ 200, { 'Content-Type': 'application/json' }, JSON.stringify({ acknowledged: true }), ]); + server.respondWith(`${API_BASE_PATH}/indices/reload`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(indices), ]); + server.respondImmediately = true; }); afterEach(() => { @@ -168,83 +192,124 @@ describe('index table', () => { server.restore(); }); - test('should change pages when a pagination link is clicked on', () => { + test('should change pages when a pagination link is clicked on', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + snapshot(namesText(rendered)); + const pagingButtons = rendered.find('.euiPaginationButton'); pagingButtons.at(2).simulate('click'); - rendered.update(); snapshot(namesText(rendered)); }); - test('should show more when per page value is increased', () => { + + test('should show more when per page value is increased', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); perPageButton.simulate('click'); rendered.update(); + const fiftyButton = rendered.find('.euiContextMenuItem').at(1); fiftyButton.simulate('click'); rendered.update(); expect(namesText(rendered).length).toBe(50); }); - test('should show the Actions menu button only when at least one row is selected', () => { + + test('should show the Actions menu button only when at least one row is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + let button = findTestSubject(rendered, 'indexTableContextMenuButton'); expect(button.length).toEqual(0); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.length).toEqual(1); }); - test('should update the Actions menu button text when more than one row is selected', () => { + + test('should update the Actions menu button text when more than one row is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + let button = findTestSubject(rendered, 'indexTableContextMenuButton'); expect(button.length).toEqual(0); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.text()).toEqual('Manage index'); + checkboxes.at(1).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.text()).toEqual('Manage 2 indices'); }); - test('should show system indices only when the switch is turned on', () => { + + test('should show system indices only when the switch is turned on', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + snapshot(rendered.find('.euiPagination li').map((item) => item.text())); const switchControl = rendered.find('.euiSwitch__button'); switchControl.simulate('click'); snapshot(rendered.find('.euiPagination li').map((item) => item.text())); }); - test('should filter based on content of search input', () => { + + test('should filter based on content of search input', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const searchInput = rendered.find('.euiFieldSearch').first(); searchInput.instance().value = 'testy0'; searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); rendered.update(); snapshot(namesText(rendered)); }); - test('should sort when header is clicked', () => { + + test('should sort when header is clicked', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const nameHeader = findTestSubject(rendered, 'indexTableHeaderCell-name').find('button'); nameHeader.simulate('click'); rendered.update(); snapshot(namesText(rendered)); + nameHeader.simulate('click'); rendered.update(); snapshot(namesText(rendered)); }); - test('should open the index detail slideout when the index name is clicked', () => { + + test('should open the index detail slideout when the index name is clicked', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(0); + const indexNameLink = names(rendered).at(0); indexNameLink.simulate('click'); rendered.update(); expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(1); }); - test('should show the right context menu options when one index is selected and open', () => { + + test('should show the right context menu options when one index is selected and open', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); @@ -253,8 +318,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when one index is selected and closed', () => { + + test('should show the right context menu options when one index is selected and closed', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(1).simulate('change', { target: { checked: true } }); rendered.update(); @@ -263,8 +332,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when one open and one closed index is selected', () => { + + test('should show the right context menu options when one open and one closed index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } }); @@ -274,8 +347,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when more than one open index is selected', () => { + + test('should show the right context menu options when more than one open index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); checkboxes.at(2).simulate('change', { target: { checked: true } }); @@ -285,8 +362,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when more than one closed index is selected', () => { + + test('should show the right context menu options when more than one closed index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(3).simulate('change', { target: { checked: true } }); @@ -296,37 +377,57 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('flush button works from context menu', (done) => { - testAction(8, done); + + test('flush button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 8); }); - test('clear cache button works from context menu', (done) => { - testAction(7, done); + + test('clear cache button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 7); }); - test('refresh button works from context menu', (done) => { - testAction(6, done); + + test('refresh button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 6); }); - test('force merge button works from context menu', (done) => { + + test('force merge button works from context menu', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const rowIndex = 0; openMenuAndClickButton(rendered, rowIndex, 5); snapshot(status(rendered, rowIndex)); expect(rendered.find('.euiModal').length).toBe(1); + let count = 0; store.subscribe(() => { - if (count > 1) { + if (count === 1) { snapshot(status(rendered, rowIndex)); expect(rendered.find('.euiModal').length).toBe(0); - done(); } count++; }); + const confirmButton = findTestSubject(rendered, 'confirmModalConfirmButton'); confirmButton.simulate('click'); snapshot(status(rendered, rowIndex)); }); - // Commenting the following 2 tests as it works in the browser (status changes to "closed" or "open") but the - // snapshot say the contrary. Need to be investigated. - test('close index button works from context menu', (done) => { + + test('close index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const modifiedIndices = indices.map((index) => { return { ...index, @@ -339,32 +440,56 @@ describe('index table', () => { { 'Content-Type': 'application/json' }, JSON.stringify(modifiedIndices), ]); - testAction(4, done); + + testAction(rendered, 4); }); - test('open index button works from context menu', (done) => { + + test('open index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const modifiedIndices = indices.map((index) => { return { ...index, status: index.name === 'testy1' ? 'open' : index.status, }; }); + server.respondWith(`${API_BASE_PATH}/indices/reload`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(modifiedIndices), ]); - testAction(3, done, 1); + + testAction(rendered, 3, 1); }); - test('show settings button works from context menu', () => { - testEditor(0); + + test('show settings button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 0); }); - test('show mappings button works from context menu', () => { - testEditor(1); + + test('show mappings button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 1); }); - test('show stats button works from context menu', () => { - testEditor(2); + + test('show stats button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 2); }); - test('edit index button works from context menu', () => { - testEditor(3); + + test('edit index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 3); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index 8c8f7e5789925..dee15f2ae3a45 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -165,8 +165,10 @@ describe('', () => { const { exists, find } = testBed; expect(exists('componentTemplatesLoadError')).toBe(true); + // The text here looks weird because the child elements' text values (title and description) + // are concatenated when we retrive the error element's text value. expect(find('componentTemplatesLoadError').text()).toContain( - 'Unable to load component templates. Try again.' + 'Error loading component templatesInternal server error' ); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 2bb240e6b6ae1..77668f7d55072 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -13,8 +13,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ScopedHistory } from 'kibana/public'; import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; -import { attemptToURIDecode } from '../../../../shared_imports'; -import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; +import { + APP_WRAPPER_CLASS, + PageLoading, + PageError, + attemptToURIDecode, +} from '../../../../shared_imports'; +import { ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; import { useComponentTemplatesContext } from '../component_templates_context'; import { @@ -24,7 +29,6 @@ import { } from '../component_template_details'; import { EmptyPrompt } from './empty_prompt'; import { ComponentTable } from './table'; -import { LoadError } from './error'; import { ComponentTemplatesDeleteModal } from './delete_modal'; interface Props { @@ -138,18 +142,20 @@ export const ComponentTemplateList: React.FunctionComponent = ({ } }, [componentTemplateName, removeContentFromGlobalFlyout]); - let content: React.ReactNode; - if (isLoading) { - content = ( - + return ( + - + ); - } else if (data?.length) { + } + + let content: React.ReactNode; + + if (data?.length) { content = ( <> @@ -183,11 +189,22 @@ export const ComponentTemplateList: React.FunctionComponent = ({ } else if (data && data.length === 0) { content = ; } else if (error) { - content = ; + content = ( + + } + error={error} + data-test-subj="componentTemplatesLoadError" + /> + ); } return ( -
    +
    {content} {/* delete modal */} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx deleted file mode 100644 index 9fd0031fe8778..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx +++ /dev/null @@ -1,40 +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, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiCallOut } from '@elastic/eui'; - -export interface Props { - onReloadClick: () => void; -} - -export const LoadError: FunctionComponent = ({ onReloadClick }) => { - return ( - - - - ), - }} - /> - } - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx index a0f6dc4b59fe7..eecb56768df9a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx @@ -9,10 +9,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { - SectionError, + PageLoading, + PageError, useAuthorizationContext, WithPrivileges, - SectionLoading, NotAuthorizedSection, } from '../shared_imports'; import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants'; @@ -26,7 +26,7 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({ if (apiError) { return ( - { if (isLoading) { return ( - + - + ); } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx index b87b043c924a6..d19c500c3622a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx @@ -10,7 +10,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SectionLoading, attemptToURIDecode } from '../../shared_imports'; +import { PageLoading, attemptToURIDecode } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; import { ComponentTemplateCreate } from '../component_template_create'; @@ -30,7 +30,8 @@ export const ComponentTemplateClone: FunctionComponent { if (error && !isLoading) { - toasts.addError(error, { + // Toasts expects a generic Error object, which is typed as having a required name property. + toasts.addError({ ...error, name: '' } as Error, { title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', { defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`, values: { sourceComponentTemplateName }, @@ -42,12 +43,12 @@ export const ComponentTemplateClone: FunctionComponent + - + ); } else { // We still show the create form (unpopulated) even if we were not able to load the diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx index 5163c75bdbadd..8fe2c193daa0c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx @@ -8,7 +8,7 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui'; import { ComponentTemplateDeserialized } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; @@ -59,27 +59,28 @@ export const ComponentTemplateCreate: React.FunctionComponent - - -

    + + -

    -
    - - - - -
    - + + } + bottomBorder + /> + + + + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 809fac980069f..6ac831b5dacce 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -8,13 +8,15 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiPageContentBody, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { useComponentTemplatesContext } from '../../component_templates_context'; import { ComponentTemplateDeserialized, - SectionLoading, + PageLoading, + PageError, attemptToURIDecode, + Error, } from '../../shared_imports'; import { ComponentTemplateForm } from '../component_template_form'; @@ -65,64 +67,57 @@ export const ComponentTemplateEdit: React.FunctionComponent + return ( + - - ); - } else if (error) { - content = ( - <> - - } - color="danger" - iconType="alert" - data-test-subj="loadComponentTemplateError" - > -
    {error.message}
    -
    - - +
    ); - } else if (componentTemplate) { - content = ( - + } + error={error as Error} + data-test-subj="loadComponentTemplateError" /> ); } return ( - - - -

    + + -

    -
    - - {content} -
    -
    + + } + bottomBorder + /> + + + + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 75c68e71996b8..6bf6d204fd9a5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -10,7 +10,6 @@ import { ComponentTemplateListItem, ComponentTemplateDeserialized, ComponentTemplateSerialized, - Error, } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, @@ -26,7 +25,7 @@ export const getApi = ( trackMetric: (type: UiCounterMetricType, eventName: string) => void ) => { function useLoadComponentTemplates() { - return useRequest({ + return useRequest({ path: `${apiBasePath}/component_templates`, method: 'get', }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts index 64b2e6b47e5d9..a7056e27b5cad 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts @@ -14,6 +14,7 @@ import { SendRequestResponse, sendRequest as _sendRequest, useRequest as _useRequest, + Error, } from '../shared_imports'; export type UseRequestHook = ( diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index afc7aed874387..15528f5b4e8e5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -12,10 +12,12 @@ export { SendRequestResponse, sendRequest, useRequest, - SectionLoading, WithPrivileges, AuthorizationProvider, SectionError, + SectionLoading, + PageLoading, + PageError, Error, useAuthorizationContext, NotAuthorizedSection, diff --git a/x-pack/plugins/index_management/public/application/components/index.ts b/x-pack/plugins/index_management/public/application/components/index.ts index f5c58e5b45ebd..eeba6e16b543c 100644 --- a/x-pack/plugins/index_management/public/application/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/index.ts @@ -6,9 +6,7 @@ */ export { SectionError, Error } from './section_error'; -export { SectionLoading } from './section_loading'; export { NoMatch } from './no_match'; -export { PageErrorForbidden } from './page_error'; export { TemplateDeleteModal } from './template_delete_modal'; export { TemplateForm } from './template_form'; export { DataHealth } from './data_health'; diff --git a/x-pack/plugins/index_management/public/application/components/page_error/index.ts b/x-pack/plugins/index_management/public/application/components/page_error/index.ts deleted file mode 100644 index 040edfa362c63..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/page_error/index.ts +++ /dev/null @@ -1,8 +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 { PageErrorForbidden } from './page_error_forbidden'; diff --git a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx deleted file mode 100644 index e22b180881ed5..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export function PageErrorForbidden() { - return ( - - - - - } - /> - - ); -} diff --git a/x-pack/plugins/index_management/public/application/components/section_loading.tsx b/x-pack/plugins/index_management/public/application/components/section_loading.tsx deleted file mode 100644 index 3c31744dee398..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/section_loading.tsx +++ /dev/null @@ -1,24 +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 { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; - -interface Props { - children: React.ReactNode; -} - -export const SectionLoading: React.FunctionComponent = ({ children }) => { - return ( - } - body={{children}} - data-test-subj="sectionLoading" - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 54160141827d0..4ccd77d275a94 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -8,7 +8,7 @@ import React, { useState, useCallback, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButton } from '@elastic/eui'; +import { EuiSpacer, EuiButton, EuiPageHeader } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateDeserialized } from '../../../../common'; @@ -292,7 +292,7 @@ export const TemplateForm = ({ return ( <> {/* Form header */} - {title} + {title}} bottomBorder /> diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index a9258c6a3b10b..3d5f56c08f8e1 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -24,8 +24,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { reactRouterNavigate } from '../../../../../shared_imports'; -import { SectionLoading, SectionError, Error, DataHealth } from '../../../../components'; +import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports'; +import { SectionError, Error, DataHealth } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index 131dc2662bc1c..7bd7c163837d8 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -16,18 +16,22 @@ import { EuiText, EuiIconTip, EuiSpacer, + EuiPageContent, EuiEmptyPrompt, EuiLink, } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { + PageLoading, + PageError, + Error, reactRouterNavigate, extractQueryParams, attemptToURIDecode, + APP_WRAPPER_CLASS, } from '../../../../shared_imports'; import { useAppContext } from '../../../app_context'; -import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; import { documentationService } from '../../../services/documentation'; import { Section } from '../home'; @@ -166,16 +170,16 @@ export const DataStreamList: React.FunctionComponent + - + ); } else if (error) { content = ( - ); - } else if (Array.isArray(dataStreams) && dataStreams.length > 0) { - activateHiddenFilter(isSelectedDataStreamHidden(dataStreams, decodedDataStreamName)); + } else { + activateHiddenFilter(isSelectedDataStreamHidden(dataStreams!, decodedDataStreamName)); content = ( - <> + {renderHeader()} @@ -270,12 +274,12 @@ export const DataStreamList: React.FunctionComponent - + ); } return ( -
    +
    {content} {/* diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx index ac46b5dbd256b..fc68ca33e9536 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { APP_WRAPPER_CLASS } from '../../../../shared_imports'; import { DetailPanel } from './detail_panel'; import { IndexTable } from './index_table'; export const IndexList: React.FunctionComponent = ({ history }) => { return ( -
    +
    diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index f488290692e7e..0a407927e3466 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -19,7 +19,7 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, - EuiLoadingSpinner, + EuiPageContent, EuiScreenReaderOnly, EuiSpacer, EuiSearchBar, @@ -37,13 +37,18 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; -import { reactRouterNavigate, attemptToURIDecode } from '../../../../../shared_imports'; +import { + PageLoading, + PageError, + reactRouterNavigate, + attemptToURIDecode, +} from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; import { getDataStreamDetailsLink } from '../../../../services/routing'; import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; -import { NoMatch, PageErrorForbidden, DataHealth } from '../../../../components'; +import { NoMatch, DataHealth } from '../../../../components'; import { IndexActionsContextMenu } from '../index_actions_context_menu'; const HEADERS = { @@ -332,42 +337,6 @@ export class IndexTable extends Component { }); } - renderError() { - const { indicesError } = this.props; - - const data = indicesError.body ? indicesError.body : indicesError; - - const { error: errorString, cause, message } = data; - - return ( - - - } - color="danger" - iconType="alert" - > -
    {message || errorString}
    - {cause && ( - - -
      - {cause.map((message, i) => ( -
    • {message}
    • - ))} -
    -
    - )} -
    - -
    - ); - } - renderBanners(extensionsService) { const { allIndices = [], filterChanged } = this.props; return extensionsService.banners.map((bannerExtension, i) => { @@ -470,37 +439,71 @@ export class IndexTable extends Component { } = this.props; const { includeHiddenIndices } = this.readURLParams(); + const hasContent = !indicesLoading && !indicesError; - let emptyState; + if (!hasContent) { + const renderNoContent = () => { + if (indicesLoading) { + return ( + + + + ); + } + + if (indicesError) { + if (indicesError.status === 403) { + return ( + + } + /> + ); + } - if (indicesLoading) { - emptyState = ( - - - - - - ); - } + return ( + + } + error={indicesError.body} + /> + ); + } + }; - if (!indicesLoading && !indicesError) { - emptyState = ; + return ( + + {renderNoContent()} + + ); } const { selectedIndicesMap } = this.state; const atLeastOneItemSelected = Object.keys(selectedIndicesMap).length > 0; - if (indicesError && indicesError.status === 403) { - return ; - } - return ( {({ services }) => { const { extensionsService } = services; return ( - + @@ -557,8 +560,6 @@ export class IndexTable extends Component { {this.renderBanners(extensionsService)} - {indicesError && this.renderError()} - {atLeastOneItemSelected ? ( @@ -665,13 +666,13 @@ export class IndexTable extends Component {
    ) : ( - emptyState + )} {indices.length > 0 ? this.renderPager() : null} - + ); }} diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index e61362efb8c99..1a82cb3bfbdd1 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -33,8 +33,8 @@ import { UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB, } from '../../../../../../common/constants'; -import { UseRequestResponse } from '../../../../../shared_imports'; -import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; +import { SectionLoading, UseRequestResponse } from '../../../../../shared_imports'; +import { TemplateDeleteModal, SectionError, Error } from '../../../../components'; import { useLoadIndexTemplate } from '../../../../services/api'; import { useServices } from '../../../../app_context'; import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index b8b5a8e3c7d1a..57f18134be5d6 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -24,13 +24,14 @@ import { import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; import { TemplateListItem } from '../../../../../common'; -import { attemptToURIDecode } from '../../../../shared_imports'; import { - SectionError, - SectionLoading, - Error, - LegacyIndexTemplatesDeprecation, -} from '../../../components'; + APP_WRAPPER_CLASS, + PageLoading, + PageError, + attemptToURIDecode, + reactRouterNavigate, +} from '../../../../shared_imports'; +import { LegacyIndexTemplatesDeprecation } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; import { documentationService } from '../../../services/documentation'; import { useServices } from '../../../app_context'; @@ -130,7 +131,8 @@ export const TemplateList: React.FunctionComponent ( - + // flex-grow: 0 is needed here because the parent element is a flex column and the header would otherwise expand. + ); - const renderContent = () => { - if (isLoading) { - return ( - + // Track this component mounted. + useEffect(() => { + uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); + }, [uiMetricService]); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + - - ); - } else if (error) { - return ( - + ); + } else if (!hasTemplates) { + content = ( + - } - error={error as Error} - /> - ); - } else if (!hasTemplates) { - return ( - + + } + body={ + <> +

    - - } - data-test-subj="emptyPrompt" - /> - ); - } else { - return ( - - {/* Header */} - {renderHeader()} +

    + + } + actions={ + + + + } + data-test-subj="emptyPrompt" + /> + ); + } else { + content = ( + <> + {/* Header */} + {renderHeader()} - {/* Composable index templates table */} - {renderTemplatesTable()} + {/* Composable index templates table */} + {renderTemplatesTable()} - {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */} - {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()} - - ); - } - }; + {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */} + {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()} - // Track component loaded - useEffect(() => { - uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); - }, [uiMetricService]); + {isTemplateDetailsVisible && ( + + )} + + ); + } return ( -
    - {renderContent()} - - {isTemplateDetailsVisible && ( - - )} +
    + {content}
    ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 36bff298e345b..32c84bc3b15f1 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -8,11 +8,12 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; +import { PageLoading, PageError, Error } from '../../../shared_imports'; import { TemplateDeserialized } from '../../../../common'; -import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; +import { TemplateForm } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; @@ -62,24 +63,22 @@ export const TemplateClone: React.FunctionComponent { breadcrumbService.setBreadcrumbs('templateClone'); }, []); if (isLoading) { - content = ( - + return ( + - + ); } else if (templateToCloneError) { - content = ( - ); - } else if (templateToClone) { - const templateData = { - ...templateToClone, - name: `${decodedTemplateName}-copy`, - } as TemplateDeserialized; + } + + const templateData = { + ...templateToClone, + name: `${decodedTemplateName}-copy`, + } as TemplateDeserialized; - content = ( + return ( + -

    - -

    - + } defaultValue={templateData} onSave={onSave} @@ -117,12 +114,6 @@ export const TemplateClone: React.FunctionComponent - ); - } - - return ( - - {content} - +
    ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index 310807aeef38f..6eba112b11939 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import { ScopedHistory } from 'kibana/public'; @@ -52,34 +52,28 @@ export const TemplateCreate: React.FunctionComponent = ({ h }, []); return ( - - - -

    - {isLegacy ? ( - - ) : ( - - )} -

    - - } - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - isLegacy={isLegacy} - history={history as ScopedHistory} - /> -
    -
    + + + ) : ( + + ) + } + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + isLegacy={isLegacy} + history={history as ScopedHistory} + /> + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index f4ffe97931a24..ff6909d4666f8 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -7,16 +7,17 @@ import React, { useEffect, useState, Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiPageContentBody, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateDeserialized } from '../../../../common'; -import { attemptToURIDecode } from '../../../shared_imports'; +import { PageError, PageLoading, attemptToURIDecode, Error } from '../../../shared_imports'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { getTemplateDetailsLink } from '../../services/routing'; -import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; +import { TemplateForm } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { @@ -62,27 +63,27 @@ export const TemplateEdit: React.FunctionComponent + return ( + - + ); } else if (error) { - content = ( - } - error={error as Error} + error={error} data-test-subj="sectionError" /> ); @@ -91,80 +92,75 @@ export const TemplateEdit: React.FunctionComponent } - color="danger" - iconType="alert" + error={ + { + message: i18n.translate( + 'xpack.idxMgmt.templateEdit.managedTemplateWarningDescription', + { + defaultMessage: 'Managed templates are critical for internal operations.', + } + ), + } as Error + } data-test-subj="systemTemplateEditCallout" - > - - + /> ); - } else { - content = ( + } + } + + return ( + + {isSystemTemplate && ( - {isSystemTemplate && ( - - - } - color="danger" - iconType="alert" - data-test-subj="systemTemplateEditCallout" - > - - - - - )} - -

    - -

    - + } - defaultValue={template} - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - isEditing={true} - isLegacy={isLegacy} - history={history as ScopedHistory} - /> + color="danger" + iconType="alert" + data-test-subj="systemTemplateEditCallout" + > + + +
    - ); - } - } + )} - return ( - - {content} - + + } + defaultValue={template!} + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + isEditing={true} + isLegacy={isLegacy} + history={history as ScopedHistory} + /> +
    ); }; diff --git a/x-pack/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts index f4d3426439562..3b1d5cf22452d 100644 --- a/x-pack/plugins/index_management/public/application/services/use_request.ts +++ b/x-pack/plugins/index_management/public/application/services/use_request.ts @@ -11,6 +11,7 @@ import { UseRequestConfig, sendRequest as _sendRequest, useRequest as _useRequest, + Error, } from '../../shared_imports'; import { httpService } from './http'; @@ -19,6 +20,6 @@ export const sendRequest = (config: SendRequestConfig): Promise(config: UseRequestConfig) => { - return _useRequest(httpService.httpClient, config); +export const useRequest = (config: UseRequestConfig) => { + return _useRequest(httpService.httpClient, config); }; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index eddac8e4b8a86..fa27b22e502fa 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -5,6 +5,8 @@ * 2.0. */ +export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; + export { SendRequestConfig, SendRequestResponse, @@ -16,6 +18,10 @@ export { extractQueryParams, GlobalFlyout, attemptToURIDecode, + PageLoading, + PageError, + Error, + SectionLoading, } from '../../../../src/plugins/es_ui_shared/public'; export { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index e164ebb4a0132..adc628be6ec4e 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -17,30 +17,42 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -export function registerGetAllRoute({ router }: RouteDependencies) { +export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) { router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); - - const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate', { - include_type_name: true, - }); - const { index_templates: templatesEs } = await callAsCurrentUser( - 'dataManagement.getComposableIndexTemplates' - ); - - const legacyTemplates = deserializeLegacyTemplateList( - legacyTemplatesEs, - cloudManagedTemplatePrefix - ); - const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); - - const body = { - templates, - legacyTemplates, - }; - - return res.ok({ body }); + + try { + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); + + const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate', { + include_type_name: true, + }); + const { index_templates: templatesEs } = await callAsCurrentUser( + 'dataManagement.getComposableIndexTemplates' + ); + + const legacyTemplates = deserializeLegacyTemplateList( + legacyTemplatesEs, + cloudManagedTemplatePrefix + ); + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); + + const body = { + templates, + legacyTemplates, + }; + + return res.ok({ body }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + // Case: default + throw error; + } }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 85916f8ac30db..86191936469db 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9801,8 +9801,6 @@ "xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink": "詳細情報", "xpack.idxMgmt.home.componentTemplates.emptyPromptTitle": "コンポーネントテンプレートを作成して開始", "xpack.idxMgmt.home.componentTemplates.list.componentTemplatesDescription": "コンポーネントテンプレートを使用して、複数のインデックステンプレートで設定、マッピング、エイリアス構成を再利用します。{learnMoreLink}", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel": "再試行してください。", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle": "コンポーネントテンプレートを読み込めません。{reloadLink}", "xpack.idxMgmt.home.componentTemplates.list.loadingMessage": "コンポーネントテンプレートを読み込んでいます…", "xpack.idxMgmt.home.componentTemplatesTabTitle": "コンポーネントテンプレート", "xpack.idxMgmt.home.dataStreamsTabTitle": "データストリーム", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 23ddc4e293ac0..daa98e063ba5f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9908,8 +9908,6 @@ "xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink": "了解详情。", "xpack.idxMgmt.home.componentTemplates.emptyPromptTitle": "首先创建组件模板", "xpack.idxMgmt.home.componentTemplates.list.componentTemplatesDescription": "使用组件模板可在多个索引模板中重复使用设置、映射和别名。{learnMoreLink}", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel": "请重试。", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle": "无法加载组件模板。{reloadLink}", "xpack.idxMgmt.home.componentTemplates.list.loadingMessage": "正在加载组件模板……", "xpack.idxMgmt.home.componentTemplatesTabTitle": "组件模板", "xpack.idxMgmt.home.dataStreamsTabTitle": "数据流", From a2f1ecc00f8e6020f883f1d379180078af28585b Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 22 Jun 2021 19:35:47 -0700 Subject: [PATCH 090/118] [App Search] Migrate Source Engines & Crawler pages to new page template (#102848) (#103030) * Convert meta engines Source Engines view to new page template * Convert CrawlerLanding to new page template * Convert CrawlerOverview to new page template * Update routers * Misc Source Engines UI polish - move away from color=secondary, EUI is eventually deprecating it - add (+) icon to match other views * Fix bad merge conflict # Conflicts: # x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx # x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx # x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx # x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx --- .../components/crawler/crawler_landing.tsx | 26 +++++++---------- .../crawler/crawler_router.test.tsx | 7 ----- .../components/crawler/crawler_router.tsx | 6 ---- .../components/engine/engine_router.tsx | 20 ++++++------- .../components/add_source_engines_button.tsx | 2 +- .../source_engines/source_engines.test.tsx | 20 +++---------- .../source_engines/source_engines.tsx | 29 +++++++++---------- 7 files changed, 39 insertions(+), 71 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx index a2993b4d86d5a..91a0a7c5edcc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx @@ -7,29 +7,25 @@ import React from 'react'; -import { - EuiButton, - EuiLink, - EuiPageHeader, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiButton, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes'; -import { generateEnginePath } from '../engine'; +import { generateEnginePath, getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import './crawler_landing.scss'; import { CRAWLER_TITLE } from '.'; export const CrawlerLanding: React.FC = () => ( -
    - - - + +

    @@ -81,5 +77,5 @@ export const CrawlerLanding: React.FC = () => (

    -
    + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx index 9f7c76d6191e2..6c5842081c81b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx @@ -5,9 +5,6 @@ * 2.0. */ -import { setMockValues } from '../../../__mocks__/kea_logic'; -import { mockEngineValues } from '../../__mocks__'; - import React from 'react'; import { Switch } from 'react-router-dom'; @@ -18,10 +15,6 @@ import { CrawlerRouter } from './crawler_router'; describe('CrawlerRouter', () => { beforeEach(() => { - setMockValues({ ...mockEngineValues }); - }); - - afterEach(() => { jest.clearAllMocks(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx index fcc949de7d8b4..9d2d2e5109eee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx @@ -8,18 +8,12 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - -import { getEngineBreadcrumbs } from '../engine'; - -import { CRAWLER_TITLE } from './constants'; import { CrawlerLanding } from './crawler_landing'; export const CrawlerRouter: React.FC = () => { return ( - 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 04e252e44270b..fa024d50d027d 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 @@ -114,6 +114,16 @@ export const EngineRouter: React.FC = () => { )} + {canViewMetaEngineSourceEngines && ( + + + + )} + {canViewEngineCrawler && ( + + + + )} {canManageEngineRelevanceTuning && ( @@ -146,16 +156,6 @@ export const EngineRouter: React.FC = () => { )} - {canViewMetaEngineSourceEngines && ( - - - - )} - {canViewEngineCrawler && ( - - - - )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx index 004217d88987b..3076e14d6329b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx @@ -18,7 +18,7 @@ export const AddSourceEnginesButton: React.FC = () => { const { openModal } = useActions(SourceEnginesLogic); return ( - + {ADD_SOURCE_ENGINES_BUTTON_LABEL} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx index 9d2fe653150c3..e2398209e630d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx @@ -11,11 +11,9 @@ import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - -import { Loading } from '../../../shared/loading'; +import { getPageHeaderActions } from '../../../test_helpers'; import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; @@ -61,20 +59,10 @@ describe('SourceEngines', () => { expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1); }); - it('renders a loading component before data has loaded', () => { - setMockValues({ ...MOCK_VALUES, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - describe('page actions', () => { - const getPageHeader = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).dive().children().dive(); - it('contains a button to add source engines', () => { const wrapper = shallow(); - expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1); + expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(1); }); it('hides the add source engines button if the user does not have permissions', () => { @@ -86,7 +74,7 @@ describe('SourceEngines', () => { }); const wrapper = shallow(); - expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0); + expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(0); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx index 190c44c919020..d2476faf4f3f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx @@ -9,13 +9,11 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiPageContent } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; import { AppLogic } from '../../app_logic'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; import { SOURCE_ENGINES_TITLE } from './i18n'; @@ -33,20 +31,19 @@ export const SourceEngines: React.FC = () => { fetchSourceEngines(); }, []); - if (dataLoading) return ; - return ( - <> - - ] : []} - /> - - + ] : [], + }} + isLoading={dataLoading} + > + {isModalOpen && } - - + + ); }; From 420c90c908f14fcc9fb2189831f4b21f9a6faa97 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 22:37:49 -0400 Subject: [PATCH 091/118] [App Search] Convert Curations pages to new page template (#102835) (#103031) * Update CurationRouter - Remove breadcrumbs set in router (will get set by page template) - Set up a curation breadcrumb helper for DRYness - Remove NotFound route - curation ID 404 handling will be used instead * Convert Curations page to new page template + move Empty State from table to top level * Convert Curation creation page to new page template * Convert single Curation page to new page template + remove breadcrumb prop * Update router * [Polish] Copy changes from Davey - see https://github.com/elastic/kibana/pull/101958/files - Per https://elastic.github.io/eui/#/guidelines/writing we shouldn't be using "new", so I removed that also * [UI polish] Add plus icon to create button - To match other create buttons across app Co-authored-by: Constance --- .../components/curations/constants.ts | 2 +- .../curations/curation/curation.test.tsx | 38 ++++++------------ .../curations/curation/curation.tsx | 38 +++++++----------- .../curation/documents/hidden_documents.tsx | 2 +- .../curations/curations_router.test.tsx | 2 +- .../components/curations/curations_router.tsx | 14 +------ .../components/curations/utils.test.ts | 16 +++++++- .../app_search/components/curations/utils.ts | 8 ++++ .../views/curation_creation.test.tsx | 1 + .../curations/views/curation_creation.tsx | 18 +++++---- .../curations/views/curations.test.tsx | 35 ++++++++-------- .../components/curations/views/curations.tsx | 40 ++++++++++--------- .../components/engine/engine_router.tsx | 10 ++--- 13 files changed, 110 insertions(+), 114 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts index 37c1e9a7a1a2e..c490910184a69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts @@ -18,7 +18,7 @@ export const CURATIONS_OVERVIEW_TITLE = i18n.translate( ); export const CREATE_NEW_CURATION_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.create.title', - { defaultMessage: 'Create new curation' } + { defaultMessage: 'Create a curation' } ); export const MANAGE_CURATION_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.manage.title', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index 937acfd84ce83..2efe1f2ffe86f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -8,16 +8,13 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; import { mockUseParams } from '../../../../__mocks__/react_router'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { Loading } from '../../../../shared/loading'; -import { rerender } from '../../../../test_helpers'; +import { rerender, getPageTitle, getPageHeaderActions } from '../../../../test_helpers'; jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); import { CurationLogic } from './curation_logic'; @@ -27,9 +24,6 @@ import { AddResultFlyout } from './results'; import { Curation } from './'; describe('Curation', () => { - const props = { - curationsBreadcrumb: ['Engines', 'some-engine', 'Curations'], - }; const values = { dataLoading: false, queries: ['query A', 'query B'], @@ -47,39 +41,34 @@ describe('Curation', () => { }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Manage curation'); - expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([ - ...props.curationsBreadcrumb, + expect(getPageTitle(wrapper)).toEqual('Manage curation'); + expect(wrapper.prop('pageChrome')).toEqual([ + 'Engines', + 'some-engine', + 'Curations', 'query A, query B', ]); }); - it('renders a loading component on page load', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders the add result flyout when open', () => { setMockValues({ ...values, isFlyoutOpen: true }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(AddResultFlyout)).toHaveLength(1); }); it('initializes CurationLogic with a curationId prop from URL param', () => { mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' }); - shallow(); + shallow(); expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' }); }); it('calls loadCuration on page load & whenever the curationId URL param changes', () => { mockUseParams.mockReturnValueOnce({ curationId: 'cur-123456789' }); - const wrapper = shallow(); + const wrapper = shallow(); expect(actions.loadCuration).toHaveBeenCalledTimes(1); mockUseParams.mockReturnValueOnce({ curationId: 'cur-987654321' }); @@ -92,9 +81,8 @@ describe('Curation', () => { let confirmSpy: jest.SpyInstance; beforeAll(() => { - const wrapper = shallow(); - const headerActions = wrapper.find(EuiPageHeader).prop('rightSideItems'); - restoreDefaultsButton = shallow(headerActions![0] as React.ReactElement); + const wrapper = shallow(); + restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0); confirmSpy = jest.spyOn(window, 'confirm'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index ffa9fd8422a1b..2a01c0db049ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,26 +10,19 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; - -import { FlashMessages } from '../../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; -import { Loading } from '../../../../shared/loading'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants'; +import { AppSearchPageTemplate } from '../../layout'; import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants'; +import { getCurationsBreadcrumbs } from '../utils'; import { CurationLogic } from './curation_logic'; import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents'; import { ActiveQuerySelect, ManageQueriesModal } from './queries'; import { AddResultLogic, AddResultFlyout } from './results'; -interface Props { - curationsBreadcrumb: BreadcrumbTrail; -} - -export const Curation: React.FC = ({ curationsBreadcrumb }) => { +export const Curation: React.FC = () => { const { curationId } = useParams() as { curationId: string }; const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId })); const { dataLoading, queries } = useValues(CurationLogic({ curationId })); @@ -39,14 +32,12 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => { loadCuration(); }, [curationId]); - if (dataLoading) return ; - return ( - <> - - { @@ -55,10 +46,10 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => { > {RESTORE_DEFAULTS_BUTTON_LABEL} , - ]} - responsive={false} - /> - + ], + }} + isLoading={dataLoading} + > @@ -69,7 +60,6 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => { - @@ -78,6 +68,6 @@ export const Curation: React.FC = ({ curationsBreadcrumb }) => { {isFlyoutOpen && } - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx index f2bc416b00341..8cb06f32d9e4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx @@ -80,7 +80,7 @@ export const HiddenDocuments: React.FC = () => {

    {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyTitle', - { defaultMessage: 'No documents are being hidden for this query' } + { defaultMessage: "You haven't hidden any documents yet" } )}

    } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx index 9598212d3e0c9..a241edb8020a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx @@ -19,6 +19,6 @@ describe('CurationsRouter', () => { const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(4); + expect(wrapper.find(Route)).toHaveLength(3); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx index 28ce311b43887..40f2d07ab61ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx @@ -8,38 +8,26 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { NotFound } from '../../../shared/not_found'; import { ENGINE_CURATIONS_PATH, ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH, } from '../../routes'; -import { getEngineBreadcrumbs } from '../engine'; -import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants'; import { Curation } from './curation'; import { Curations, CurationCreation } from './views'; export const CurationsRouter: React.FC = () => { - const CURATIONS_BREADCRUMB = getEngineBreadcrumbs([CURATIONS_TITLE]); - return ( - - - - - - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts index 51618ed4e3741..02641b09255e5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts @@ -5,7 +5,21 @@ * 2.0. */ -import { convertToDate, addDocument, removeDocument } from './utils'; +import '../../__mocks__/engine_logic.mock'; + +import { getCurationsBreadcrumbs, convertToDate, addDocument, removeDocument } from './utils'; + +describe('getCurationsBreadcrumbs', () => { + it('generates curation-prefixed breadcrumbs', () => { + expect(getCurationsBreadcrumbs()).toEqual(['Engines', 'some-engine', 'Curations']); + expect(getCurationsBreadcrumbs(['Some page'])).toEqual([ + 'Engines', + 'some-engine', + 'Curations', + 'Some page', + ]); + }); +}); describe('convertToDate', () => { it('converts the English-only server timestamps to a parseable Date', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts index 8af2636128304..978b63885fbdd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts @@ -5,6 +5,14 @@ * 2.0. */ +import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs'; +import { getEngineBreadcrumbs } from '../engine'; + +import { CURATIONS_TITLE } from './constants'; + +export const getCurationsBreadcrumbs = (breadcrumbs: BreadcrumbTrail = []) => + getEngineBreadcrumbs([CURATIONS_TITLE, ...breadcrumbs]); + // The server API feels us an English datestring, but we want to convert // it to an actual Date() instance so that we can localize date formats. export const convertToDate = (serverDateString: string): Date => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx index ad306dfc73080..33aab9943cc83 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx @@ -6,6 +6,7 @@ */ import { setMockActions } from '../../../../__mocks__/kea_logic'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx index 32d46775a2125..9aa1759cec5c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx @@ -9,10 +9,10 @@ import React from 'react'; import { useActions } from 'kea'; -import { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../../shared/flash_messages'; +import { AppSearchPageTemplate } from '../../layout'; import { MultiInputRows } from '../../multi_input_rows'; import { @@ -21,15 +21,17 @@ import { QUERY_INPUTS_PLACEHOLDER, } from '../constants'; import { CurationsLogic } from '../index'; +import { getCurationsBreadcrumbs } from '../utils'; export const CurationCreation: React.FC = () => { const { createCuration } = useActions(CurationsLogic); return ( - <> - - - + +

    {i18n.translate( @@ -56,7 +58,7 @@ export const CurationCreation: React.FC = () => { inputPlaceholder={QUERY_INPUTS_PLACEHOLDER} onSubmit={(queries) => createCuration(queries)} /> - - + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index bcc402d6eea27..85827d5374179 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -6,17 +6,16 @@ */ import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/react_router'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ReactWrapper } from 'enzyme'; -import { EuiPageHeader, EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable } from '@elastic/eui'; -import { Loading } from '../../../../shared/loading'; -import { mountWithIntl } from '../../../../test_helpers'; -import { EmptyState } from '../components'; +import { mountWithIntl, getPageTitle } from '../../../../test_helpers'; import { Curations, CurationsTable } from './curations'; @@ -61,32 +60,34 @@ describe('Curations', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Curated results'); + expect(getPageTitle(wrapper)).toEqual('Curated results'); expect(wrapper.find(CurationsTable)).toHaveLength(1); }); - it('renders a loading component on page load', () => { - setMockValues({ ...values, dataLoading: true, curations: [] }); - const wrapper = shallow(); + describe('loading state', () => { + it('renders a full-page loading state on initial page load', () => { + setMockValues({ ...values, dataLoading: true, curations: [] }); + const wrapper = shallow(); + + expect(wrapper.prop('isLoading')).toEqual(true); + }); + + it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => { + setMockValues({ ...values, dataLoading: true, curations: [{}] }); + const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(false); + }); }); it('calls loadCurations on page load', () => { + setMockValues({ ...values, myRole: {} }); // Required for AppSearchPageTemplate to load mountWithIntl(); expect(actions.loadCurations).toHaveBeenCalledTimes(1); }); describe('CurationsTable', () => { - it('renders an empty state', () => { - setMockValues({ ...values, curations: [] }); - const table = shallow().find(EuiBasicTable); - const noItemsMessage = table.prop('noItemsMessage') as React.ReactElement; - - expect(noItemsMessage.type).toEqual(EmptyState); - }); - it('passes loading prop based on dataLoading', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 80de9aba77258..12497ab52baf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -9,25 +9,24 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiBasicTable, EuiBasicTableColumn, EuiPageContent, EuiPageHeader } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants'; -import { FlashMessages } from '../../../../shared/flash_messages'; import { KibanaLogic } from '../../../../shared/kibana'; -import { Loading } from '../../../../shared/loading'; import { EuiButtonTo, EuiLinkTo } from '../../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes'; import { FormattedDateTime } from '../../../utils/formatted_date_time'; import { generateEnginePath } from '../../engine'; +import { AppSearchPageTemplate } from '../../layout'; import { EmptyState } from '../components'; import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants'; import { CurationsLogic } from '../curations_logic'; import { Curation } from '../types'; -import { convertToDate } from '../utils'; +import { getCurationsBreadcrumbs, convertToDate } from '../utils'; export const Curations: React.FC = () => { const { dataLoading, curations, meta } = useValues(CurationsLogic); @@ -37,23 +36,29 @@ export const Curations: React.FC = () => { loadCurations(); }, [meta.page.current]); - if (dataLoading && !curations.length) return ; - return ( - <> - + {CREATE_NEW_CURATION_TITLE} , - ]} - /> - - + ], + }} + isLoading={dataLoading && !curations.length} + isEmptyState={!curations.length} + emptyState={} + > + - - + + ); }; @@ -139,7 +144,6 @@ export const CurationsTable: React.FC = () => { responsive hasActions loading={dataLoading} - noItemsMessage={} pagination={{ ...convertMetaToPagination(meta), hidePerPageOptions: true, 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 fa024d50d027d..59535fb737fa6 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 @@ -129,6 +129,11 @@ export const EngineRouter: React.FC = () => { )} + {canManageEngineCurations && ( + + + + )} {canManageEngineResultSettings && ( @@ -146,11 +151,6 @@ export const EngineRouter: React.FC = () => { )} {/* TODO: Remove layout once page template migration is over */} }> - {canManageEngineCurations && ( - - - - )} {canManageEngineSynonyms && ( From 1d540912814bab24cdb773bd7b66951c654b68ef Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 22 Jun 2021 23:02:33 -0400 Subject: [PATCH 092/118] [Uptime] Refactor cert alerts from batched to individual (#102138) (#103035) * refactor cert alerts from batched to individual * remove old translations * create new certificate alert rule type and transition old cert rule type to legacy * update translations * maintain legacy tls rule UI to support legacy rule editing * update translations * update TLS alert content, rule type id, and alert instance id schema * remove extraneous logic and format date content Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dominique Clarke --- .../translations/translations/ja-JP.json | 3 +- .../translations/translations/zh-CN.json | 3 +- .../plugins/uptime/common/constants/alerts.ts | 15 +- .../uptime/public/lib/alert_types/index.ts | 2 + .../public/lib/alert_types/tls_legacy.tsx | 32 ++++ .../public/lib/alert_types/translations.ts | 22 ++- .../plugins/uptime/server/lib/alerts/index.ts | 4 +- .../uptime/server/lib/alerts/tls.test.ts | 94 +++++------ .../plugins/uptime/server/lib/alerts/tls.ts | 135 ++++++++------- .../server/lib/alerts/tls_legacy.test.ts | 139 ++++++++++++++++ .../uptime/server/lib/alerts/tls_legacy.ts | 156 ++++++++++++++++++ .../uptime/server/lib/alerts/translations.ts | 17 +- .../apps/uptime/alert_flyout.ts | 2 +- 13 files changed, 495 insertions(+), 129 deletions(-) create mode 100644 x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx create mode 100644 x-pack/plugins/uptime/server/lib/alerts/tls_legacy.test.ts create mode 100644 x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 86191936469db..efffd2b6d7b0f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23505,7 +23505,6 @@ "xpack.uptime.alerts.tls.criteriaExpression.ariaLabel": "このアラートで監視されるモニターの条件を示す式", "xpack.uptime.alerts.tls.criteriaExpression.description": "タイミング", "xpack.uptime.alerts.tls.criteriaExpression.value": "任意のモニター", - "xpack.uptime.alerts.tls.defaultActionMessage": "期限切れになるか古くなりすぎた{count} TLS個のTLS証明書証明書を検知しました。\n\n{expiringConditionalOpen}\n期限切れになる証明書数:{expiringCount}\n期限切れになる証明書:{expiringCommonNameAndDate}\n{expiringConditionalClose}\n\n{agingConditionalOpen}\n古い証明書数:{agingCount}\n古い証明書:{agingCommonNameAndDate}\n{agingConditionalClose}\n", "xpack.uptime.alerts.tls.description": "アップタイム監視の TLS 証明書の有効期限が近いときにアラートを発行します。", "xpack.uptime.alerts.tls.expirationExpression.ariaLabel": "証明書有効期限の TLS アラートをトリガーするしきい値を示す式", "xpack.uptime.alerts.tls.expirationExpression.description": "証明書が", @@ -24356,4 +24355,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index daa98e063ba5f..d2916c4ef8704 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23871,7 +23871,6 @@ "xpack.uptime.alerts.tls.criteriaExpression.ariaLabel": "显示此告警监视的监测条件的表达式", "xpack.uptime.alerts.tls.criteriaExpression.description": "当", "xpack.uptime.alerts.tls.criteriaExpression.value": "任意监测", - "xpack.uptime.alerts.tls.defaultActionMessage": "已检测到 {count} 个即将过期或即将过时的 TLS 证书。\n\n{expiringConditionalOpen}\n即将过期的证书计数:{expiringCount}\n即将过期的证书:{expiringCommonNameAndDate}\n{expiringConditionalClose}\n\n{agingConditionalOpen}\n过时的证书计数:{agingCount}\n过时的证书:{agingCommonNameAndDate}\n{agingConditionalClose}\n", "xpack.uptime.alerts.tls.description": "运行时间监测的 TLS 证书即将过期时告警。", "xpack.uptime.alerts.tls.expirationExpression.ariaLabel": "显示将触发证书过期 TLS 告警的阈值的表达式", "xpack.uptime.alerts.tls.expirationExpression.description": "具有将在", @@ -24732,4 +24731,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts index 37258fca3bc4d..cb31d83839590 100644 --- a/x-pack/plugins/uptime/common/constants/alerts.ts +++ b/x-pack/plugins/uptime/common/constants/alerts.ts @@ -8,7 +8,8 @@ import { ActionGroup } from '../../../alerting/common'; export type MonitorStatusActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; -export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; +export type TLSLegacyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; +export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tlsCertificate'>; export type DurationAnomalyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; export const MONITOR_STATUS: MonitorStatusActionGroup = { @@ -16,8 +17,13 @@ export const MONITOR_STATUS: MonitorStatusActionGroup = { name: 'Uptime Down Monitor', }; -export const TLS: TLSActionGroup = { +export const TLS_LEGACY: TLSLegacyActionGroup = { id: 'xpack.uptime.alerts.actionGroups.tls', + name: 'Uptime TLS Alert (Legacy)', +}; + +export const TLS: TLSActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.tlsCertificate', name: 'Uptime TLS Alert', }; @@ -28,16 +34,19 @@ export const DURATION_ANOMALY: DurationAnomalyActionGroup = { export const ACTION_GROUP_DEFINITIONS: { MONITOR_STATUS: MonitorStatusActionGroup; + TLS_LEGACY: TLSLegacyActionGroup; TLS: TLSActionGroup; DURATION_ANOMALY: DurationAnomalyActionGroup; } = { MONITOR_STATUS, + TLS_LEGACY, TLS, DURATION_ANOMALY, }; export const CLIENT_ALERT_TYPES = { MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus', - TLS: 'xpack.uptime.alerts.tls', + TLS_LEGACY: 'xpack.uptime.alerts.tls', + TLS: 'xpack.uptime.alerts.tlsCertificate', DURATION_ANOMALY: 'xpack.uptime.alerts.durationAnomaly', }; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts index 36c84fe4c64cd..406b730fa1e6c 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/index.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts @@ -9,6 +9,7 @@ import { CoreStart } from 'kibana/public'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { initMonitorStatusAlertType } from './monitor_status'; import { initTlsAlertType } from './tls'; +import { initTlsLegacyAlertType } from './tls_legacy'; import { ClientPluginsStart } from '../../apps/plugin'; import { initDurationAnomalyAlertType } from './duration_anomaly'; @@ -20,5 +21,6 @@ export type AlertTypeInitializer = (dependenies: { export const alertTypeInitializers: AlertTypeInitializer[] = [ initMonitorStatusAlertType, initTlsAlertType, + initTlsLegacyAlertType, initDurationAnomalyAlertType, ]; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx new file mode 100644 index 0000000000000..1abcdb2c98662 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.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 from 'react'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; +import { TlsTranslationsLegacy } from './translations'; +import { AlertTypeInitializer } from '.'; + +const { defaultActionMessage, description } = TlsTranslationsLegacy; +const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); +export const initTlsLegacyAlertType: AlertTypeInitializer = ({ + core, + plugins, +}): AlertTypeModel => ({ + id: CLIENT_ALERT_TYPES.TLS_LEGACY, + iconClass: 'uptimeApp', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/uptime/${docLinks.DOC_LINK_VERSION}/uptime-alerting.html#_tls_alerts`; + }, + alertParamsExpression: (params: any) => ( + + ), + description, + validate: () => ({ errors: {} }), + defaultActionMessage, + requiresAppContext: false, +}); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts index ea445e3d63c09..bb4af761d240d 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts @@ -8,14 +8,32 @@ import { i18n } from '@kbn/i18n'; export const TlsTranslations = { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { + defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary} +`, + values: { + commonName: '{{state.commonName}}', + issuer: '{{state.issuer}}', + summary: '{{state.summary}}', + status: '{{state.status}}', + }, + }), + name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { + defaultMessage: 'Uptime TLS (Legacy)', + }), + description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { + defaultMessage: + 'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.', + }), +}; + +export const TlsTranslationsLegacy = { defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { defaultMessage: `Detected {count} TLS certificates expiring or becoming too old. - {expiringConditionalOpen} Expiring cert count: {expiringCount} Expiring Certificates: {expiringCommonNameAndDate} {expiringConditionalClose} - {agingConditionalOpen} Aging cert count: {agingCount} Aging Certificates: {agingCommonNameAndDate} diff --git a/x-pack/plugins/uptime/server/lib/alerts/index.ts b/x-pack/plugins/uptime/server/lib/alerts/index.ts index 1559ceaae8bb6..c695a4b052cd9 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/index.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/index.ts @@ -8,6 +8,7 @@ import { UptimeAlertTypeFactory } from './types'; import { statusCheckAlertFactory, ActionGroupIds as statusCheckActionGroup } from './status_check'; import { tlsAlertFactory, ActionGroupIds as tlsActionGroup } from './tls'; +import { tlsLegacyAlertFactory, ActionGroupIds as tlsLegacyActionGroup } from './tls_legacy'; import { durationAnomalyAlertFactory, ActionGroupIds as durationAnomalyActionGroup, @@ -16,5 +17,6 @@ import { export const uptimeAlertTypeFactories: [ UptimeAlertTypeFactory, UptimeAlertTypeFactory, + UptimeAlertTypeFactory, UptimeAlertTypeFactory -] = [statusCheckAlertFactory, tlsAlertFactory, durationAnomalyAlertFactory]; +] = [statusCheckAlertFactory, tlsAlertFactory, tlsLegacyAlertFactory, durationAnomalyAlertFactory]; diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts index dde6ef8535365..a77fe10f0b9a4 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts @@ -23,6 +23,7 @@ describe('tls alert', () => { common_name: 'Common-One', monitors: [{ name: 'monitor-one', id: 'monitor1' }], sha256: 'abc', + issuer: 'Cloudflare Inc ECC CA-3', }, { not_after: '2020-07-18T03:15:39.000Z', @@ -30,6 +31,7 @@ describe('tls alert', () => { common_name: 'Common-Two', monitors: [{ name: 'monitor-two', id: 'monitor2' }], sha256: 'bcd', + issuer: 'Cloudflare Inc ECC CA-3', }, { not_after: '2020-07-19T03:15:39.000Z', @@ -37,6 +39,7 @@ describe('tls alert', () => { common_name: 'Common-Three', monitors: [{ name: 'monitor-three', id: 'monitor3' }], sha256: 'cde', + issuer: 'Cloudflare Inc ECC CA-3', }, { not_after: '2020-07-25T03:15:39.000Z', @@ -44,6 +47,7 @@ describe('tls alert', () => { common_name: 'Common-Four', monitors: [{ name: 'monitor-four', id: 'monitor4' }], sha256: 'def', + issuer: 'Cloudflare Inc ECC CA-3', }, ]; }); @@ -52,88 +56,66 @@ describe('tls alert', () => { jest.clearAllMocks(); }); - it('sorts expiring certs appropriately when creating summary', () => { - diffSpy.mockReturnValueOnce(900).mockReturnValueOnce(901).mockReturnValueOnce(902); + it('handles positive diffs for expired certs appropriately', () => { + diffSpy.mockReturnValueOnce(900); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-20T05:00:00.000Z').valueOf(), new Date('2019-03-01T00:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "", - "agingCount": 0, - "count": 4, - "expiringCommonNameAndDate": "Common-One, expired on 2020-07-16T03:15:39.000Z 900 days ago.; Common-Two, expired on 2020-07-18T03:15:39.000Z 901 days ago.; Common-Three, expired on 2020-07-19T03:15:39.000Z 902 days ago.", - "expiringCount": 3, - "hasAging": null, - "hasExpired": true, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'expired on Jul 15, 2020 EDT, 900 days ago.', + status: 'expired', + }); }); - it('sorts aging certs appropriate when creating summary', () => { - diffSpy.mockReturnValueOnce(702).mockReturnValueOnce(701).mockReturnValueOnce(700); + it('handles positive diffs for agining certs appropriately', () => { + diffSpy.mockReturnValueOnce(702); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-01T12:00:00.000Z').valueOf(), new Date('2019-09-01T03:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 702 days ago.; Common-Three, valid since 2019-07-22T03:15:39.000Z, 701 days ago.; Common-One, valid since 2019-07-24T03:15:39.000Z, 700 days ago.", - "agingCount": 4, - "count": 4, - "expiringCommonNameAndDate": "", - "expiringCount": 0, - "hasAging": true, - "hasExpired": null, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'valid since Jul 23, 2019 EDT, 702 days ago.', + status: 'becoming too old', + }); }); it('handles negative diff values appropriately for aging certs', () => { - diffSpy.mockReturnValueOnce(700).mockReturnValueOnce(-90).mockReturnValueOnce(-80); + diffSpy.mockReturnValueOnce(-90); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-01T12:00:00.000Z').valueOf(), new Date('2019-09-01T03:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 700 days ago.; Common-Three, invalid until 2019-07-22T03:15:39.000Z, 90 days from now.; Common-One, invalid until 2019-07-24T03:15:39.000Z, 80 days from now.", - "agingCount": 4, - "count": 4, - "expiringCommonNameAndDate": "", - "expiringCount": 0, - "hasAging": true, - "hasExpired": null, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'invalid until Jul 23, 2019 EDT, 90 days from now.', + status: 'invalid', + }); }); it('handles negative diff values appropriately for expiring certs', () => { diffSpy // negative days are in the future, positive days are in the past - .mockReturnValueOnce(-96) - .mockReturnValueOnce(-94) - .mockReturnValueOnce(2); + .mockReturnValueOnce(-96); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-20T05:00:00.000Z').valueOf(), new Date('2019-03-01T00:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "", - "agingCount": 0, - "count": 4, - "expiringCommonNameAndDate": "Common-One, expires on 2020-07-16T03:15:39.000Z in 96 days.; Common-Two, expires on 2020-07-18T03:15:39.000Z in 94 days.; Common-Three, expired on 2020-07-19T03:15:39.000Z 2 days ago.", - "expiringCount": 3, - "hasAging": null, - "hasExpired": true, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'expires on Jul 15, 2020 EDT in 96 days.', + status: 'expiring', + }); }); }); }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 2a2406a3629d0..f29744fdbb70f 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -22,71 +22,80 @@ export type ActionGroupIds = ActionGroupIdsOf; const DEFAULT_SIZE = 20; interface TlsAlertState { - count: number; - agingCount: number; - agingCommonNameAndDate: string; - expiringCount: number; - expiringCommonNameAndDate: string; - hasAging: true | null; - hasExpired: true | null; + commonName: string; + issuer: string; + summary: string; + status: string; } -const sortCerts = (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf(); +interface TLSContent { + summary: string; + status?: string; +} const mapCertsToSummaryString = ( - certs: Cert[], - certLimitMessage: (cert: Cert) => string, - maxSummaryItems: number -): string => - certs - .slice(0, maxSummaryItems) - .map((cert) => `${cert.common_name}, ${certLimitMessage(cert)}`) - .reduce((prev, cur) => (prev === '' ? cur : prev.concat(`; ${cur}`)), ''); - -const getValidAfter = ({ not_after: date }: Cert) => { - if (!date) return 'Error, missing `certificate_not_valid_after` date.'; + cert: Cert, + certLimitMessage: (cert: Cert) => TLSContent +): TLSContent => certLimitMessage(cert); + +const getValidAfter = ({ not_after: date }: Cert): TLSContent => { + if (!date) return { summary: 'Error, missing `certificate_not_valid_after` date.' }; const relativeDate = moment().diff(date, 'days'); + const formattedDate = moment(date).format('MMM D, YYYY z'); return relativeDate >= 0 - ? tlsTranslations.validAfterExpiredString(date, relativeDate) - : tlsTranslations.validAfterExpiringString(date, Math.abs(relativeDate)); + ? { + summary: tlsTranslations.validAfterExpiredString(formattedDate, relativeDate), + status: tlsTranslations.expiredLabel, + } + : { + summary: tlsTranslations.validAfterExpiringString(formattedDate, Math.abs(relativeDate)), + status: tlsTranslations.expiringLabel, + }; }; -const getValidBefore = ({ not_before: date }: Cert): string => { - if (!date) return 'Error, missing `certificate_not_valid_before` date.'; +const getValidBefore = ({ not_before: date }: Cert): TLSContent => { + if (!date) return { summary: 'Error, missing `certificate_not_valid_before` date.' }; const relativeDate = moment().diff(date, 'days'); + const formattedDate = moment(date).format('MMM D, YYYY z'); return relativeDate >= 0 - ? tlsTranslations.validBeforeExpiredString(date, relativeDate) - : tlsTranslations.validBeforeExpiringString(date, Math.abs(relativeDate)); + ? { + summary: tlsTranslations.validBeforeExpiredString(formattedDate, relativeDate), + status: tlsTranslations.agingLabel, + } + : { + summary: tlsTranslations.validBeforeExpiringString(formattedDate, Math.abs(relativeDate)), + status: tlsTranslations.invalidLabel, + }; }; export const getCertSummary = ( - certs: Cert[], + cert: Cert, expirationThreshold: number, - ageThreshold: number, - maxSummaryItems: number = 3 + ageThreshold: number ): TlsAlertState => { - certs.sort((a, b) => sortCerts(a.not_after ?? '', b.not_after ?? '')); - const expiring = certs.filter( - (cert) => new Date(cert.not_after ?? '').valueOf() < expirationThreshold - ); + const isExpiring = new Date(cert.not_after ?? '').valueOf() < expirationThreshold; + const isAging = new Date(cert.not_before ?? '').valueOf() < ageThreshold; + let content: TLSContent | null = null; + + if (isExpiring) { + content = mapCertsToSummaryString(cert, getValidAfter); + } else if (isAging) { + content = mapCertsToSummaryString(cert, getValidBefore); + } - certs.sort((a, b) => sortCerts(a.not_before ?? '', b.not_before ?? '')); - const aging = certs.filter((cert) => new Date(cert.not_before ?? '').valueOf() < ageThreshold); + const { summary = '', status = '' } = content || {}; return { - count: certs.length, - agingCount: aging.length, - agingCommonNameAndDate: mapCertsToSummaryString(aging, getValidBefore, maxSummaryItems), - expiringCommonNameAndDate: mapCertsToSummaryString(expiring, getValidAfter, maxSummaryItems), - expiringCount: expiring.length, - hasAging: aging.length > 0 ? true : null, - hasExpired: expiring.length > 0 ? true : null, + commonName: cert.common_name ?? '', + issuer: cert.issuer ?? '', + summary, + status, }; }; export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => uptimeAlertWrapper({ - id: 'xpack.uptime.alerts.tls', + id: 'xpack.uptime.alerts.tlsCertificate', name: tlsTranslations.alertFactoryName, validate: { params: schema.object({}), @@ -129,26 +138,30 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, const foundCerts = total > 0; if (foundCerts) { - const absoluteExpirationThreshold = moment() - .add( - dynamicSettings.certExpirationThreshold ?? - DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, - 'd' - ) - .valueOf(); - const absoluteAgeThreshold = moment() - .subtract( - dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, - 'd' - ) - .valueOf(); - const alertInstance = alertInstanceFactory(TLS.id); - const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); - alertInstance.replaceState({ - ...updateState(state, foundCerts), - ...summary, + certs.forEach((cert) => { + const absoluteExpirationThreshold = moment() + .add( + dynamicSettings.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, + 'd' + ) + .valueOf(); + const absoluteAgeThreshold = moment() + .subtract( + dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, + 'd' + ) + .valueOf(); + const alertInstance = alertInstanceFactory( + `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}` + ); + const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold); + alertInstance.replaceState({ + ...updateState(state, foundCerts), + ...summary, + }); + alertInstance.scheduleActions(TLS.id); }); - alertInstance.scheduleActions(TLS.id); } return updateState(state, foundCerts); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.test.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.test.ts new file mode 100644 index 0000000000000..4c6a721e92159 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.test.ts @@ -0,0 +1,139 @@ +/* + * 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 moment from 'moment'; +import { getCertSummary } from './tls_legacy'; +import { Cert } from '../../../common/runtime_types'; + +describe('tls alert', () => { + describe('getCertSummary', () => { + let mockCerts: Cert[]; + let diffSpy: jest.SpyInstance; + + beforeEach(() => { + diffSpy = jest.spyOn(moment.prototype, 'diff'); + mockCerts = [ + { + not_after: '2020-07-16T03:15:39.000Z', + not_before: '2019-07-24T03:15:39.000Z', + common_name: 'Common-One', + monitors: [{ name: 'monitor-one', id: 'monitor1' }], + sha256: 'abc', + }, + { + not_after: '2020-07-18T03:15:39.000Z', + not_before: '2019-07-20T03:15:39.000Z', + common_name: 'Common-Two', + monitors: [{ name: 'monitor-two', id: 'monitor2' }], + sha256: 'bcd', + }, + { + not_after: '2020-07-19T03:15:39.000Z', + not_before: '2019-07-22T03:15:39.000Z', + common_name: 'Common-Three', + monitors: [{ name: 'monitor-three', id: 'monitor3' }], + sha256: 'cde', + }, + { + not_after: '2020-07-25T03:15:39.000Z', + not_before: '2019-07-25T03:15:39.000Z', + common_name: 'Common-Four', + monitors: [{ name: 'monitor-four', id: 'monitor4' }], + sha256: 'def', + }, + ]; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('sorts expiring certs appropriately when creating summary', () => { + diffSpy.mockReturnValueOnce(900).mockReturnValueOnce(901).mockReturnValueOnce(902); + const result = getCertSummary( + mockCerts, + new Date('2020-07-20T05:00:00.000Z').valueOf(), + new Date('2019-03-01T00:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "", + "agingCount": 0, + "count": 4, + "expiringCommonNameAndDate": "Common-One, expired on 2020-07-16T03:15:39.000Z, 900 days ago.; Common-Two, expired on 2020-07-18T03:15:39.000Z, 901 days ago.; Common-Three, expired on 2020-07-19T03:15:39.000Z, 902 days ago.", + "expiringCount": 3, + "hasAging": null, + "hasExpired": true, + } + `); + }); + + it('sorts aging certs appropriate when creating summary', () => { + diffSpy.mockReturnValueOnce(702).mockReturnValueOnce(701).mockReturnValueOnce(700); + const result = getCertSummary( + mockCerts, + new Date('2020-07-01T12:00:00.000Z').valueOf(), + new Date('2019-09-01T03:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 702 days ago.; Common-Three, valid since 2019-07-22T03:15:39.000Z, 701 days ago.; Common-One, valid since 2019-07-24T03:15:39.000Z, 700 days ago.", + "agingCount": 4, + "count": 4, + "expiringCommonNameAndDate": "", + "expiringCount": 0, + "hasAging": true, + "hasExpired": null, + } + `); + }); + + it('handles negative diff values appropriately for aging certs', () => { + diffSpy.mockReturnValueOnce(700).mockReturnValueOnce(-90).mockReturnValueOnce(-80); + const result = getCertSummary( + mockCerts, + new Date('2020-07-01T12:00:00.000Z').valueOf(), + new Date('2019-09-01T03:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 700 days ago.; Common-Three, invalid until 2019-07-22T03:15:39.000Z, 90 days from now.; Common-One, invalid until 2019-07-24T03:15:39.000Z, 80 days from now.", + "agingCount": 4, + "count": 4, + "expiringCommonNameAndDate": "", + "expiringCount": 0, + "hasAging": true, + "hasExpired": null, + } + `); + }); + + it('handles negative diff values appropriately for expiring certs', () => { + diffSpy + // negative days are in the future, positive days are in the past + .mockReturnValueOnce(-96) + .mockReturnValueOnce(-94) + .mockReturnValueOnce(2); + const result = getCertSummary( + mockCerts, + new Date('2020-07-20T05:00:00.000Z').valueOf(), + new Date('2019-03-01T00:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "", + "agingCount": 0, + "count": 4, + "expiringCommonNameAndDate": "Common-One, expires on 2020-07-16T03:15:39.000Z in 96 days.; Common-Two, expires on 2020-07-18T03:15:39.000Z in 94 days.; Common-Three, expired on 2020-07-19T03:15:39.000Z, 2 days ago.", + "expiringCount": 3, + "hasAging": null, + "hasExpired": true, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts new file mode 100644 index 0000000000000..8f1c0093e60ac --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts @@ -0,0 +1,156 @@ +/* + * 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 moment from 'moment'; +import { schema } from '@kbn/config-schema'; +import { UptimeAlertTypeFactory } from './types'; +import { updateState } from './common'; +import { TLS_LEGACY } from '../../../common/constants/alerts'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { Cert, CertResult } from '../../../common/runtime_types'; +import { commonStateTranslations, tlsTranslations } from './translations'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; +import { uptimeAlertWrapper } from './uptime_alert_wrapper'; +import { ActionGroupIdsOf } from '../../../../alerting/common'; + +export type ActionGroupIds = ActionGroupIdsOf; + +const DEFAULT_SIZE = 20; + +interface TlsAlertState { + count: number; + agingCount: number; + agingCommonNameAndDate: string; + expiringCount: number; + expiringCommonNameAndDate: string; + hasAging: true | null; + hasExpired: true | null; +} + +const sortCerts = (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf(); + +const mapCertsToSummaryString = ( + certs: Cert[], + certLimitMessage: (cert: Cert) => string, + maxSummaryItems: number +): string => + certs + .slice(0, maxSummaryItems) + .map((cert) => `${cert.common_name}, ${certLimitMessage(cert)}`) + .reduce((prev, cur) => (prev === '' ? cur : prev.concat(`; ${cur}`)), ''); + +const getValidAfter = ({ not_after: date }: Cert) => { + if (!date) return 'Error, missing `certificate_not_valid_after` date.'; + const relativeDate = moment().diff(date, 'days'); + return relativeDate >= 0 + ? tlsTranslations.validAfterExpiredString(date, relativeDate) + : tlsTranslations.validAfterExpiringString(date, Math.abs(relativeDate)); +}; + +const getValidBefore = ({ not_before: date }: Cert): string => { + if (!date) return 'Error, missing `certificate_not_valid_before` date.'; + const relativeDate = moment().diff(date, 'days'); + return relativeDate >= 0 + ? tlsTranslations.validBeforeExpiredString(date, relativeDate) + : tlsTranslations.validBeforeExpiringString(date, Math.abs(relativeDate)); +}; + +export const getCertSummary = ( + certs: Cert[], + expirationThreshold: number, + ageThreshold: number, + maxSummaryItems: number = 3 +): TlsAlertState => { + certs.sort((a, b) => sortCerts(a.not_after ?? '', b.not_after ?? '')); + const expiring = certs.filter( + (cert) => new Date(cert.not_after ?? '').valueOf() < expirationThreshold + ); + + certs.sort((a, b) => sortCerts(a.not_before ?? '', b.not_before ?? '')); + const aging = certs.filter((cert) => new Date(cert.not_before ?? '').valueOf() < ageThreshold); + + return { + count: certs.length, + agingCount: aging.length, + agingCommonNameAndDate: mapCertsToSummaryString(aging, getValidBefore, maxSummaryItems), + expiringCommonNameAndDate: mapCertsToSummaryString(expiring, getValidAfter, maxSummaryItems), + expiringCount: expiring.length, + hasAging: aging.length > 0 ? true : null, + hasExpired: expiring.length > 0 ? true : null, + }; +}; + +export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_server, libs) => + uptimeAlertWrapper({ + id: 'xpack.uptime.alerts.tls', + name: tlsTranslations.legacyAlertFactoryName, + validate: { + params: schema.object({}), + }, + defaultActionGroupId: TLS_LEGACY.id, + actionGroups: [ + { + id: TLS_LEGACY.id, + name: TLS_LEGACY.name, + }, + ], + actionVariables: { + context: [], + state: [...tlsTranslations.actionVariables, ...commonStateTranslations], + }, + minimumLicenseRequired: 'basic', + async executor({ options, dynamicSettings, uptimeEsClient }) { + const { + services: { alertInstanceFactory }, + state, + } = options; + + const { certs, total }: CertResult = await libs.requests.getCerts({ + uptimeEsClient, + from: DEFAULT_FROM, + to: DEFAULT_TO, + index: 0, + size: DEFAULT_SIZE, + notValidAfter: `now+${ + dynamicSettings?.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold + }d`, + notValidBefore: `now-${ + dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold + }d`, + sortBy: 'common_name', + direction: 'desc', + }); + + const foundCerts = total > 0; + + if (foundCerts) { + const absoluteExpirationThreshold = moment() + .add( + dynamicSettings.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, + 'd' + ) + .valueOf(); + const absoluteAgeThreshold = moment() + .subtract( + dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, + 'd' + ) + .valueOf(); + const alertInstance = alertInstanceFactory(TLS_LEGACY.id); + const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); + alertInstance.replaceState({ + ...updateState(state, foundCerts), + ...summary, + }); + alertInstance.scheduleActions(TLS_LEGACY.id); + } + + return updateState(state, foundCerts); + }, + }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/translations.ts b/x-pack/plugins/uptime/server/lib/alerts/translations.ts index 3630185e19ab0..ee356eb68a626 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/translations.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/translations.ts @@ -151,6 +151,9 @@ export const tlsTranslations = { alertFactoryName: i18n.translate('xpack.uptime.alerts.tls', { defaultMessage: 'Uptime TLS', }), + legacyAlertFactoryName: i18n.translate('xpack.uptime.alerts.tlsLegacy', { + defaultMessage: 'Uptime TLS (Legacy)', + }), actionVariables: [ { name: 'count', @@ -191,7 +194,7 @@ export const tlsTranslations = { ], validAfterExpiredString: (date: string, relativeDate: number) => i18n.translate('xpack.uptime.alerts.tls.validAfterExpiredString', { - defaultMessage: `expired on {date} {relativeDate} days ago.`, + defaultMessage: `expired on {date}, {relativeDate} days ago.`, values: { date, relativeDate, @@ -221,6 +224,18 @@ export const tlsTranslations = { relativeDate, }, }), + expiredLabel: i18n.translate('xpack.uptime.alerts.tls.expiredLabel', { + defaultMessage: 'expired', + }), + expiringLabel: i18n.translate('xpack.uptime.alerts.tls.expiringLabel', { + defaultMessage: 'expiring', + }), + agingLabel: i18n.translate('xpack.uptime.alerts.tls.agingLabel', { + defaultMessage: 'becoming too old', + }), + invalidLabel: i18n.translate('xpack.uptime.alerts.tls.invalidLabel', { + defaultMessage: 'invalid', + }), }; export const durationAnomalyTranslations = { diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index bbd212b61e439..afc6dca936bbf 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -201,7 +201,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } = alert; try { expect(actions).to.eql([]); - expect(alertTypeId).to.eql('xpack.uptime.alerts.tls'); + expect(alertTypeId).to.eql('xpack.uptime.alerts.tlsCertificate'); expect(consumer).to.eql('uptime'); expect(tags).to.eql(['uptime', 'certs']); expect(params).to.eql({}); From 909e03551581a5564ec0c265ecccd4b391a35420 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 00:53:30 -0400 Subject: [PATCH 093/118] [App Search] Convert Synonyms page to new page template (#102828) (#103040) * Convert Synonyms page to new page template * Update empty state for new page template - Remove EuiPanel wrapper - KibanaPageTemplate does that automatically for us - Include SynonymModal, required for header create button to work as expected * Update router * [UI polish] Proposed page description copy from Davey - see https://github.com/elastic/kibana/pull/101958/commits/9807bf249abd5e4c1f88ee05f4ebafab123ceeb9 * [UI polish] Add plus icon to create button - To match other create buttons across app Co-authored-by: Constance --- .../components/engine/engine_router.tsx | 15 ++-- .../synonyms/components/empty_state.test.tsx | 8 +- .../synonyms/components/empty_state.tsx | 9 ++- .../components/synonyms/synonyms.test.tsx | 29 ++----- .../components/synonyms/synonyms.tsx | 81 ++++++++----------- 5 files changed, 60 insertions(+), 82 deletions(-) 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 59535fb737fa6..0f42483f44e0c 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 @@ -13,9 +13,7 @@ import { useValues, useActions } from 'kea'; import { i18n } from '@kbn/i18n'; import { setQueuedErrorMessage } from '../../../shared/flash_messages'; -import { Layout } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; -import { AppSearchNav } from '../../index'; import { ENGINE_PATH, @@ -129,6 +127,11 @@ export const EngineRouter: React.FC = () => { )} + {canManageEngineSynonyms && ( + + + + )} {canManageEngineCurations && ( @@ -149,14 +152,6 @@ export const EngineRouter: React.FC = () => { )} - {/* TODO: Remove layout once page template migration is over */} - }> - {canManageEngineSynonyms && ( - - - - )} - ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx index f1382bb5972b2..a43f170e5822f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; -import { EmptyState } from './'; +import { EmptyState, SynonymModal } from './'; describe('EmptyState', () => { it('renders', () => { @@ -24,4 +24,10 @@ describe('EmptyState', () => { expect.stringContaining('/synonyms-guide.html') ); }); + + it('renders the add synonym modal', () => { + const wrapper = shallow(); + + expect(wrapper.find(SynonymModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx index 2eb6643bda503..f856a5c035f81 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx @@ -7,16 +7,16 @@ import React from 'react'; -import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; -import { SynonymIcon } from './'; +import { SynonymModal, SynonymIcon } from './'; export const EmptyState: React.FC = () => { return ( - + <> { } /> - + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx index c8f65c4bdbc6c..64ac3066b51a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx @@ -13,12 +13,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader, EuiButton, EuiPagination } from '@elastic/eui'; +import { EuiButton, EuiPagination } from '@elastic/eui'; -import { Loading } from '../../../shared/loading'; -import { rerender } from '../../../test_helpers'; +import { rerender, getPageHeaderActions } from '../../../test_helpers'; -import { SynonymCard, SynonymModal, EmptyState } from './components'; +import { SynonymCard, SynonymModal } from './components'; import { Synonyms } from './'; @@ -53,21 +52,9 @@ describe('Synonyms', () => { }); it('renders a create action button', () => { - const wrapper = shallow() - .find(EuiPageHeader) - .dive() - .children() - .dive(); - - wrapper.find(EuiButton).simulate('click'); - expect(actions.openModal).toHaveBeenCalled(); - }); - - it('renders an empty state if no synonyms exist', () => { - setMockValues({ ...values, synonymSets: [] }); const wrapper = shallow(); - - expect(wrapper.find(EmptyState)).toHaveLength(1); + getPageHeaderActions(wrapper).find(EuiButton).simulate('click'); + expect(actions.openModal).toHaveBeenCalled(); }); describe('loading', () => { @@ -75,14 +62,14 @@ describe('Synonyms', () => { setMockValues({ ...values, synonymSets: [], dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); it('does not render a full loading state after initial page load', () => { setMockValues({ ...values, synonymSets: [MOCK_SYNONYM_SET], dataLoading: true }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(0); + expect(wrapper.prop('isLoading')).toEqual(false); }); }); @@ -108,7 +95,7 @@ describe('Synonyms', () => { const wrapper = shallow(); expect(actions.onPaginate).not.toHaveBeenCalled(); - expect(wrapper.find(EmptyState)).toHaveLength(1); + expect(wrapper.prop('isEmptyState')).toEqual(true); }); it('handles off-by-one shenanigans between EuiPagination and our API', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx index d3ba53819f7de..4a68bc381f764 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx @@ -9,21 +9,11 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiPageHeader, - EuiButton, - EuiPageContentBody, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, - EuiPagination, -} from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiFlexGrid, EuiFlexItem, EuiPagination } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { SynonymCard, SynonymModal, EmptyState } from './components'; import { SYNONYMS_TITLE } from './constants'; @@ -46,46 +36,45 @@ export const Synonyms: React.FC = () => { } }, [synonymSets]); - if (dataLoading && !hasSynonyms) return ; - return ( - <> - - openModal(null)}> + openModal(null)}> {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.synonyms.createSynonymSetButtonLabel', { defaultMessage: 'Create a synonym set' } )} , - ]} + ], + }} + isLoading={dataLoading && !hasSynonyms} + isEmptyState={!hasSynonyms} + emptyState={} + > + + {synonymSets.map(({ id, synonyms }) => ( + + + + ))} + + + onPaginate(pageIndex + 1)} /> - - - - {hasSynonyms ? ( - <> - - {synonymSets.map(({ id, synonyms }) => ( - - - - ))} - - - onPaginate(pageIndex + 1)} - /> - - ) : ( - - )} - - - + + ); }; From 04b6326e5d5c9f5d8207f1fc702f9f638fd5c567 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 23 Jun 2021 09:46:39 +0200 Subject: [PATCH 094/118] [ML] Functional tests - temporarily skip close_jobs API tests --- x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts index 0d64008a49688..4c639d3a166cd 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts @@ -97,7 +97,8 @@ export default ({ getService }: FtrProviderContext) => { return body; } - describe('close_jobs', function () { + // failing ES snapshot promotion after backend change, see https://github.com/elastic/kibana/issues/103023 + describe.skip('close_jobs', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); From b725cf3b7447ccf49cd6e75a0a172fe55ddad62d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 05:22:36 -0400 Subject: [PATCH 095/118] [Security Solution][Endpoint] Paginate actions log with infinite scroll (#102261) (#103047) * Show loading below the list when loading fixes elastic/security-team/issues/1245 * use intersection observer to load data when callout is visible fixes elastic/security-team/issues/1245 * remove unused `total` from API response refs 4f7d18bee78cdc389ab0bf3cb94271c66b4ae25b * toggle ability to paging based on API response and target intersection fixes elastic/security-team/issues/1245 * use a invisible target * display a message when end of log fixes elastic/security-team/issues/1245 * remove search bar fixes elastic/security-team/issues/1245 * refresh data fixes elastic/security-team/issues/1245 * rename refs 85e5add14ebf99558d8d08d3e3fdb5ec23dfe732 * add refresh button to empty state * add translations for copy * remove refresh button * load activity log for endpoint on activity log tab selection fixes elastic/security-team/issues/1312 * reset paging correctly on activity log tab selection * fix variable mixup refs elastic/kibana/pull/101032/commits/c4e933a9c5954ce249942ca66bab380c1dfa79e2#diff-41a74ad41665921620230a0729728f3bf6e27a6f9dc302fb37b0d2061637c212R81 * fix react warning refs 697a3c3bac4979a1ffbecd397efd8dc23cf4ee80 * clean up review changes * use the complicated flyout version instead of styled version refs https://elastic.github.io/eui/#/layout/flyout#more-complicated-flyout refs https://github.com/elastic/kibana/pull/99795/files#r635810660 refs c26a7d47b4d9485ce743f67a9de16bc6fbb7f816 * Page only when scrolled (so that info message is shown after paging once) fixes https://github.com/elastic/security-team/issues/1245#issuecomment-863440335 * add tests fixes elastic/security-team/issues/1312 fixes elastic/security-team/issues/1245 * increase the parent container's height to ensure that the scroll target is well hidden below the footer refs 48e32916811618034015bde4f27c56432da96a8d * Update x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> * Update x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> * address review changes * cleanup callback and effect Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Co-authored-by: Ashokaditya Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> --- .../common/endpoint/types/actions.ts | 1 - .../pages/endpoint_hosts/store/action.ts | 19 ++- .../pages/endpoint_hosts/store/builders.ts | 8 +- .../pages/endpoint_hosts/store/index.test.ts | 8 +- .../endpoint_hosts/store/middleware.test.ts | 12 +- .../pages/endpoint_hosts/store/middleware.ts | 56 ++++--- .../pages/endpoint_hosts/store/reducer.ts | 47 +++++- .../pages/endpoint_hosts/store/selectors.ts | 11 +- .../management/pages/endpoint_hosts/types.ts | 8 +- .../components/endpoint_details_tabs.tsx | 98 ++++++++---- .../view/details/components/flyout_header.tsx | 47 ++++++ .../view/details/components/log_entry.tsx | 2 +- .../view/details/endpoint_activity_log.tsx | 123 ++++++++++----- .../view/details/endpoints.stories.tsx | 1 - .../endpoint_hosts/view/details/index.tsx | 80 ++++------ .../pages/endpoint_hosts/view/index.test.tsx | 142 ++++++++++++++++++ .../pages/endpoint_hosts/view/translations.ts | 20 +++ .../server/endpoint/routes/actions/service.ts | 5 - 18 files changed, 524 insertions(+), 164 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 99753242e7627..dfaad68e295eb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -58,7 +58,6 @@ export interface ActivityLogActionResponse { } export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; export interface ActivityLog { - total: number; page: number; pageSize: number; data: ActivityLogEntry[]; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 5b5bac3a0a6e1..949feb2964317 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -16,7 +16,7 @@ import { import { ServerApiError } from '../../../../common/types'; import { GetPolicyListResponse } from '../../policy/types'; import { GetPackagesResponse } from '../../../../../../fleet/common'; -import { EndpointState } from '../types'; +import { EndpointIndexUIQueryParams, EndpointState } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; export interface ServerReturnedEndpointList { @@ -163,12 +163,29 @@ export type EndpointPendingActionsStateChanged = Action<'endpointPendingActionsS payload: EndpointState['endpointPendingActions']; }; +export interface EndpointDetailsActivityLogUpdatePaging { + type: 'endpointDetailsActivityLogUpdatePaging'; + payload: { + // disable paging when no more data after paging + disabled: boolean; + page: number; + pageSize: number; + }; +} + +export interface EndpointDetailsFlyoutTabChanged { + type: 'endpointDetailsFlyoutTabChanged'; + payload: { flyoutView: EndpointIndexUIQueryParams['show'] }; +} + export type EndpointAction = | ServerReturnedEndpointList | ServerFailedToReturnEndpointList | ServerReturnedEndpointDetails | ServerFailedToReturnEndpointDetails | AppRequestedEndpointActivityLog + | EndpointDetailsActivityLogUpdatePaging + | EndpointDetailsFlyoutTabChanged | EndpointDetailsActivityLogChanged | ServerReturnedEndpointPolicyResponse | ServerFailedToReturnEndpointPolicyResponse diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index d43f361a0e6bb..317b735e1169e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -19,9 +19,13 @@ export const initialEndpointPageState = (): Immutable => { loading: false, error: undefined, endpointDetails: { + flyoutView: undefined, activityLog: { - page: 1, - pageSize: 50, + paging: { + disabled: false, + page: 1, + pageSize: 50, + }, logData: createUninitialisedResourceState(), }, hostDetails: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 7f7c5f84f8bff..68dd47362bc38 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -42,9 +42,13 @@ describe('EndpointList store concerns', () => { loading: false, error: undefined, endpointDetails: { + flyoutView: undefined, activityLog: { - page: 1, - pageSize: 50, + paging: { + disabled: false, + page: 1, + pageSize: 50, + }, logData: { type: 'UninitialisedResourceState' }, }, hostDetails: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 52da30fabf95a..6cf5e989fb645 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -44,6 +44,7 @@ import { } from '../../../../common/lib/endpoint_isolation/mocks'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; import { endpointPageHttpMock } from '../mocks'; +import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs'; jest.mock('../../policy/store/services/ingest', () => ({ sendGetAgentConfigList: () => Promise.resolve({ items: [] }), @@ -226,8 +227,16 @@ describe('endpoint list middleware', () => { const dispatchUserChangedUrl = () => { dispatchUserChangedUrlToEndpointList({ search: `?${search.split('?').pop()}` }); }; + const dispatchFlyoutViewChange = () => { + dispatch({ + type: 'endpointDetailsFlyoutTabChanged', + payload: { + flyoutView: EndpointDetailsTabsTypes.activityLog, + }, + }); + }; - const fleetActionGenerator = new FleetActionGenerator(Math.random().toString()); + const fleetActionGenerator = new FleetActionGenerator('seed'); const actionData = fleetActionGenerator.generate({ agents: [endpointList.hosts[0].metadata.agent.id], }); @@ -265,6 +274,7 @@ describe('endpoint list middleware', () => { it('should set ActivityLog state to loading', async () => { dispatchUserChangedUrl(); + dispatchFlyoutViewChange(); const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', { validate(action) { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 4f96223e8b789..53b30aeb02bd5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -35,6 +35,7 @@ import { getActivityLogDataPaging, getLastLoadedActivityLogData, detailsData, + getEndpointDetailsFlyoutView, } from './selectors'; import { AgentIdsPendingActions, EndpointState, PolicyIds } from '../types'; import { @@ -48,6 +49,7 @@ import { ENDPOINT_ACTION_LOG_ROUTE, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, + BASE_POLICY_RESPONSE_ROUTE, metadataCurrentIndexPattern, } from '../../../../../common/endpoint/constants'; import { IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; @@ -61,6 +63,7 @@ import { AppAction } from '../../../../common/store/actions'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; import { ServerReturnedEndpointPackageInfo } from './action'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; +import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs'; type EndpointPageStore = ImmutableMiddlewareAPI; @@ -339,6 +342,28 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(error.body ?? error), }); } - - // call the policy response api - try { - const policyResponse = await coreStart.http.get(`/api/endpoint/policy_response`, { - query: { agentId: selectedEndpoint }, - }); - dispatch({ - type: 'serverReturnedEndpointPolicyResponse', - payload: policyResponse, - }); - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointPolicyResponse', - payload: error, - }); - } } // page activity log API @@ -408,17 +417,24 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(updatedLogData), }); - // TODO dispatch 'noNewLogData' if !activityLog.length - // resets paging to previous state + if (!activityLog.data.length) { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: true, + page: activityLog.page - 1, + pageSize: activityLog.pageSize, + }, + }); + } } else { dispatch({ type: 'endpointDetailsActivityLogChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 9460c27dfe705..44c63edd8e95c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -29,12 +29,23 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer { + const pagingOptions = + action.payload.type === 'LoadedResourceState' + ? { + ...state.endpointDetails.activityLog, + paging: { + ...state.endpointDetails.activityLog.paging, + page: action.payload.data.page, + pageSize: action.payload.data.pageSize, + }, + } + : { ...state.endpointDetails.activityLog }; return { ...state!, endpointDetails: { ...state.endpointDetails!, activityLog: { - ...state.endpointDetails.activityLog, + ...pagingOptions, logData: action.payload, }, }, @@ -138,7 +149,8 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }, }; } else if (action.type === 'appRequestedEndpointActivityLog') { - const pageData = { + const paging = { + disabled: state.endpointDetails.activityLog.paging.disabled, page: action.payload.page, pageSize: action.payload.pageSize, }; @@ -148,10 +160,32 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...state.endpointDetails!, activityLog: { ...state.endpointDetails.activityLog, - ...pageData, + paging, }, }, }; + } else if (action.type === 'endpointDetailsActivityLogUpdatePaging') { + const paging = { + ...action.payload, + }; + return { + ...state, + endpointDetails: { + ...state.endpointDetails!, + activityLog: { + ...state.endpointDetails.activityLog, + paging, + }, + }, + }; + } else if (action.type === 'endpointDetailsFlyoutTabChanged') { + return { + ...state, + endpointDetails: { + ...state.endpointDetails!, + flyoutView: action.payload.flyoutView, + }, + }; } else if (action.type === 'endpointDetailsActivityLogChanged') { return handleEndpointDetailsActivityLogChanged(state, action); } else if (action.type === 'endpointPendingActionsStateChanged') { @@ -255,8 +289,11 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta const activityLog = { logData: createUninitialisedResourceState(), - page: 1, - pageSize: 50, + paging: { + disabled: false, + page: 1, + pageSize: 50, + }, }; // Reset `isolationRequestState` if needed diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index d9be85377c81d..eeb54379e8e7d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -364,13 +364,14 @@ export const getIsolationRequestError: ( } }); +export const getEndpointDetailsFlyoutView = ( + state: Immutable +): EndpointIndexUIQueryParams['show'] => state.endpointDetails.flyoutView; + export const getActivityLogDataPaging = ( state: Immutable -): Immutable> => { - return { - page: state.endpointDetails.activityLog.page, - pageSize: state.endpointDetails.activityLog.pageSize, - }; +): Immutable => { + return state.endpointDetails.activityLog.paging; }; export const getActivityLogData = ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 59aa2bd15dd74..c985259588cb0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -37,9 +37,13 @@ export interface EndpointState { /** api error from retrieving host list */ error?: ServerApiError; endpointDetails: { + flyoutView: EndpointIndexUIQueryParams['show']; activityLog: { - page: number; - pageSize: number; + paging: { + disabled: boolean; + page: number; + pageSize: number; + }; logData: AsyncResourceState; }; hostDetails: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx index 3e228be4565b1..aa1f56529657e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx @@ -5,10 +5,15 @@ * 2.0. */ +import { useDispatch } from 'react-redux'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiTab, EuiTabs, EuiFlyoutBody, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; import { EndpointIndexUIQueryParams } from '../../../types'; +import { EndpointAction } from '../../../store/action'; +import { useEndpointSelector } from '../../hooks'; +import { getActivityLogDataPaging } from '../../../store/selectors'; +import { EndpointDetailsFlyoutHeader } from './flyout_header'; + export enum EndpointDetailsTabsTypes { overview = 'overview', activityLog = 'activity_log', @@ -24,29 +29,18 @@ interface EndpointDetailsTabs { content: JSX.Element; } -const StyledEuiTabbedContent = styled(EuiTabbedContent)` - overflow: hidden; - padding-bottom: ${(props) => props.theme.eui.paddingSizes.xl}; - - > [role='tabpanel'] { - height: 100%; - padding-right: 12px; - overflow: hidden; - overflow-y: auto; - ::-webkit-scrollbar { - -webkit-appearance: none; - width: 4px; - } - ::-webkit-scrollbar-thumb { - border-radius: 2px; - background-color: rgba(0, 0, 0, 0.5); - -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); - } - } -`; - export const EndpointDetailsFlyoutTabs = memo( - ({ show, tabs }: { show: EndpointIndexUIQueryParams['show']; tabs: EndpointDetailsTabs[] }) => { + ({ + hostname, + show, + tabs, + }: { + hostname?: string; + show: EndpointIndexUIQueryParams['show']; + tabs: EndpointDetailsTabs[]; + }) => { + const dispatch = useDispatch<(action: EndpointAction) => void>(); + const { pageSize } = useEndpointSelector(getActivityLogDataPaging); const [selectedTabId, setSelectedTabId] = useState(() => { return show === 'details' ? EndpointDetailsTabsTypes.overview @@ -54,8 +48,33 @@ export const EndpointDetailsFlyoutTabs = memo( }); const handleTabClick = useCallback( - (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EndpointDetailsTabsId), - [setSelectedTabId] + (tab: EuiTabbedContentTab) => { + dispatch({ + type: 'endpointDetailsFlyoutTabChanged', + payload: { + flyoutView: tab.id as EndpointIndexUIQueryParams['show'], + }, + }); + if (tab.id === EndpointDetailsTabsTypes.activityLog) { + const paging = { + page: 1, + pageSize, + }; + dispatch({ + type: 'appRequestedEndpointActivityLog', + payload: paging, + }); + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + ...paging, + }, + }); + } + return setSelectedTabId(tab.id as EndpointDetailsTabsId); + }, + [dispatch, pageSize, setSelectedTabId] ); const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [ @@ -63,14 +82,27 @@ export const EndpointDetailsFlyoutTabs = memo( selectedTabId, ]); + const renderTabs = tabs.map((tab) => ( + handleTabClick(tab)} + isSelected={tab.id === selectedTabId} + key={tab.id} + data-test-subj={tab.id} + > + {tab.name} + + )); + return ( - + <> + + + {renderTabs} + + + {selectedTab?.content} + + ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx new file mode 100644 index 0000000000000..f791c0d6adf17 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiFlyoutHeader, EuiLoadingContent, EuiToolTip, EuiTitle } from '@elastic/eui'; +import { useEndpointSelector } from '../../hooks'; +import { detailsLoading } from '../../../store/selectors'; + +export const EndpointDetailsFlyoutHeader = memo( + ({ + hasBorder = false, + hostname, + children, + }: { + hasBorder?: boolean; + hostname?: string; + children?: React.ReactNode | React.ReactNodeArray; + }) => { + const hostDetailsLoading = useEndpointSelector(detailsLoading); + + return ( + + {hostDetailsLoading ? ( + + ) : ( + + +

    + {hostname} +

    +
    +
    + )} + {children} +
    + ); + } +); + +EndpointDetailsFlyoutHeader.displayName = 'EndpointDetailsFlyoutHeader'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx index c431cd682d25b..4fe70039d1251 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx @@ -78,7 +78,7 @@ const useLogEntryUIProps = ( if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; } else { - return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; + return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed; } } else { if (isSuccessful) { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 55479845bce0a..f1701054c4d5f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -5,11 +5,19 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useEffect, useRef } from 'react'; +import styled from 'styled-components'; -import { EuiButton, EuiEmptyPrompt, EuiLoadingContent, EuiSpacer } from '@elastic/eui'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiEmptyPrompt, +} from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { LogEntry } from './components/log_entry'; +import * as i18 from '../translations'; import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types'; import { AsyncResourceState } from '../../../../state'; import { useEndpointSelector } from '../hooks'; @@ -19,54 +27,95 @@ import { getActivityLogError, getActivityLogIterableData, getActivityLogRequestLoaded, + getLastLoadedActivityLogData, getActivityLogRequestLoading, } from '../../store/selectors'; +const LoadMoreTrigger = styled.div` + height: 6px; + width: 100%; +`; + export const EndpointActivityLog = memo( ({ activityLog }: { activityLog: AsyncResourceState> }) => { const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading); const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded); + const activityLastLogData = useEndpointSelector(getLastLoadedActivityLogData); const activityLogData = useEndpointSelector(getActivityLogIterableData); + const activityLogSize = activityLogData.length; const activityLogError = useEndpointSelector(getActivityLogError); - const dispatch = useDispatch<(a: EndpointAction) => void>(); - const { page, pageSize } = useEndpointSelector(getActivityLogDataPaging); + const dispatch = useDispatch<(action: EndpointAction) => void>(); + const { page, pageSize, disabled: isPagingDisabled } = useEndpointSelector( + getActivityLogDataPaging + ); + + const loadMoreTrigger = useRef(null); + const getActivityLog = useCallback( + (entries: IntersectionObserverEntry[]) => { + const isTargetIntersecting = entries.some((entry) => entry.isIntersecting); + if (isTargetIntersecting && activityLogLoaded && !isPagingDisabled) { + dispatch({ + type: 'appRequestedEndpointActivityLog', + payload: { + page: page + 1, + pageSize, + }, + }); + } + }, + [activityLogLoaded, dispatch, isPagingDisabled, page, pageSize] + ); - const getActivityLog = useCallback(() => { - dispatch({ - type: 'appRequestedEndpointActivityLog', - payload: { - page: page + 1, - pageSize, - }, - }); - }, [dispatch, page, pageSize]); + useEffect(() => { + const observer = new IntersectionObserver(getActivityLog); + const element = loadMoreTrigger.current; + if (element) { + observer.observe(element); + } + return () => { + observer.disconnect(); + }; + }, [getActivityLog]); return ( <> - - {activityLogLoading || activityLogError ? ( - {'No logged actions'}

    } - body={

    {'No actions have been logged for this endpoint.'}

    } - /> - ) : ( - <> - - {activityLogLoading ? ( - - ) : ( - activityLogLoaded && - activityLogData.map((logEntry) => ( - - )) - )} - - {'show more'} - - - )} + + {(activityLogLoaded && !activityLogSize) || activityLogError ? ( + + {i18.ACTIVITY_LOG.LogEntry.emptyState.title}} + body={

    {i18.ACTIVITY_LOG.LogEntry.emptyState.body}

    } + data-test-subj="activityLogEmpty" + /> +
    + ) : ( + <> + + {activityLogLoaded && + activityLogData.map((logEntry) => ( + + ))} + {activityLogLoading && + activityLastLogData?.data.map((logEntry) => ( + + ))} + + + {activityLogLoading && } + {(!activityLogLoading || !isPagingDisabled) && ( + + )} + {isPagingDisabled && !activityLogLoading && ( + +

    {i18.ACTIVITY_LOG.LogEntry.endOfLog}

    +
    + )} +
    + + )} +
    ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index d839bbfaae875..d3c91f6f18499 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -20,7 +20,6 @@ export const dummyEndpointActivityLog = ( ): AsyncResourceState> => ({ type: 'LoadedResourceState', data: { - total: 20, page: 1, pageSize: 50, data: [ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 59e0c0e787a22..e295ea145edcb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -5,21 +5,16 @@ * 2.0. */ +import { useDispatch } from 'react-redux'; import React, { useCallback, useEffect, useMemo, memo } from 'react'; -import styled from 'styled-components'; import { EuiFlyout, EuiFlyoutBody, - EuiFlyoutHeader, EuiFlyoutFooter, EuiLoadingContent, - EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, - EuiToolTip, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -30,7 +25,6 @@ import { uiQueryParams, detailsData, detailsError, - detailsLoading, getActivityLogData, showView, policyResponseConfigurations, @@ -59,23 +53,12 @@ import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpo import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding'; import { getEndpointListPath } from '../../../../common/routing'; import { ActionsMenu } from './components/actions_menu'; - -const DetailsFlyoutBody = styled(EuiFlyoutBody)` - overflow-y: hidden; - flex: 1; - - .euiFlyoutBody__overflow { - overflow: hidden; - mask-image: none; - } - - .euiFlyoutBody__overflowContent { - height: 100%; - display: flex; - } -`; +import { EndpointIndexUIQueryParams } from '../../types'; +import { EndpointAction } from '../../store/action'; +import { EndpointDetailsFlyoutHeader } from './components/flyout_header'; export const EndpointDetailsFlyout = memo(() => { + const dispatch = useDispatch<(action: EndpointAction) => void>(); const history = useHistory(); const toasts = useToasts(); const queryParams = useEndpointSelector(uiQueryParams); @@ -86,13 +69,24 @@ export const EndpointDetailsFlyout = memo(() => { const activityLog = useEndpointSelector(getActivityLogData); const hostDetails = useEndpointSelector(detailsData); - const hostDetailsLoading = useEndpointSelector(detailsLoading); const hostDetailsError = useEndpointSelector(detailsError); const policyInfo = useEndpointSelector(policyVersionInfo); const hostStatus = useEndpointSelector(hostStatusInfo); const show = useEndpointSelector(showView); + const setFlyoutView = useCallback( + (flyoutView: EndpointIndexUIQueryParams['show']) => { + dispatch({ + type: 'endpointDetailsFlyoutTabChanged', + payload: { + flyoutView, + }, + }); + }, + [dispatch] + ); + const ContentLoadingMarkup = useMemo( () => ( <> @@ -133,9 +127,11 @@ export const EndpointDetailsFlyout = memo(() => { ...urlSearchParams, }) ); - }, [history, queryParamsWithoutSelectedEndpoint]); + setFlyoutView(undefined); + }, [setFlyoutView, history, queryParamsWithoutSelectedEndpoint]); useEffect(() => { + setFlyoutView(show); if (hostDetailsError !== undefined) { toasts.addDanger({ title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', { @@ -146,7 +142,10 @@ export const EndpointDetailsFlyout = memo(() => { }), }); } - }, [hostDetailsError, toasts]); + return () => { + setFlyoutView(undefined); + }; + }, [hostDetailsError, setFlyoutView, show, toasts]); return ( { size="m" paddingSize="l" > - - {hostDetailsLoading ? ( - - ) : ( - - -

    - {hostDetails?.host?.hostname} -

    -
    -
    - )} -
    + {(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && ( + + )} {hostDetails === undefined ? ( @@ -179,13 +165,11 @@ export const EndpointDetailsFlyout = memo(() => { ) : ( <> {(show === 'details' || show === 'activity_log') && ( - - - - - - - + )} {show === 'policy_response' && } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 6aab9336c21a4..4869ce84fad2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -17,6 +17,7 @@ import { } from '../store/mock_endpoint_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { + ActivityLog, HostInfo, HostPolicyResponse, HostPolicyResponseActionStatus, @@ -32,12 +33,15 @@ import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kib import { hostIsolationHttpMocks } from '../../../../common/lib/endpoint_isolation/mocks'; import { fireEvent } from '@testing-library/dom'; import { + createFailedResourceState, + createLoadedResourceState, isFailedResourceState, isLoadedResourceState, isUninitialisedResourceState, } from '../../../state'; import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; +import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; // not sure why this can't be imported from '../../../../common/mock/formatted_relative'; // but sure enough it needs to be inline in this one file @@ -625,6 +629,30 @@ describe('when on the endpoint list page', () => { }); }; + const dispatchEndpointDetailsActivityLogChanged = ( + dataState: 'failed' | 'success', + data: ActivityLog + ) => { + reactTestingLibrary.act(() => { + const getPayload = () => { + switch (dataState) { + case 'failed': + return createFailedResourceState({ + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred.', + }); + case 'success': + return createLoadedResourceState(data); + } + }; + store.dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: getPayload(), + }); + }); + }; + beforeEach(async () => { mockEndpointListApi(); @@ -746,6 +774,120 @@ describe('when on the endpoint list page', () => { expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull(); }); + describe('when showing Activity Log panel', () => { + let renderResult: ReturnType; + const agentId = 'some_agent_id'; + + let getMockData: () => ActivityLog; + beforeEach(async () => { + window.IntersectionObserver = jest.fn(() => ({ + root: null, + rootMargin: '', + thresholds: [], + takeRecords: jest.fn(), + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + })); + + const fleetActionGenerator = new FleetActionGenerator('seed'); + const responseData = fleetActionGenerator.generateResponse({ + agent_id: agentId, + }); + const actionData = fleetActionGenerator.generate({ + agents: [agentId], + }); + getMockData = () => ({ + page: 1, + pageSize: 50, + data: [ + { + type: 'response', + item: { + id: 'some_id_0', + data: responseData, + }, + }, + { + type: 'action', + item: { + id: 'some_id_1', + data: actionData, + }, + }, + ], + }); + + renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedEndpointList'); + }); + const hostNameLinks = await renderResult.getAllByTestId('hostnameCellLink'); + reactTestingLibrary.fireEvent.click(hostNameLinks[0]); + }); + + afterEach(reactTestingLibrary.cleanup); + + it('should show the endpoint details flyout', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', getMockData()); + }); + const endpointDetailsFlyout = await renderResult.queryByTestId('endpointDetailsFlyoutBody'); + expect(endpointDetailsFlyout).not.toBeNull(); + }); + + it('should display log accurately', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', getMockData()); + }); + const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + expect(logEntries.length).toEqual(2); + expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null); + }); + + it('should display empty state when API call has failed', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('failed', getMockData()); + }); + const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + expect(emptyState).not.toBe(null); + }); + + it('should display empty state when no log data', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + data: [], + }); + }); + + const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + expect(emptyState).not.toBe(null); + }); + }); + describe('when showing host Policy Response panel', () => { let renderResult: ReturnType; beforeEach(async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index 18a5bd1e5130a..89ffd2d23807e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -16,6 +16,26 @@ export const ACTIVITY_LOG = { defaultMessage: 'Activity Log', }), LogEntry: { + endOfLog: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog', + { + defaultMessage: 'Nothing more to show', + } + ), + emptyState: { + title: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.title', + { + defaultMessage: 'No logged actions', + } + ), + body: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.body', + { + defaultMessage: 'No actions have been logged for this endpoint.', + } + ), + }, action: { isolatedAction: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.isolated', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts index 20b29694a1df1..1a8b17bf19e18 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts @@ -56,7 +56,6 @@ export const getAuditLogResponse = async ({ context: SecuritySolutionRequestHandlerContext; logger: Logger; }): Promise<{ - total: number; page: number; pageSize: number; data: Array<{ @@ -96,10 +95,6 @@ export const getAuditLogResponse = async ({ } return { - total: - typeof result.body.hits.total === 'number' - ? result.body.hits.total - : result.body.hits.total.value, page, pageSize, data: result.body.hits.hits.map((e) => ({ From c73473ed92b100797be66b826056111e23597749 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 06:03:36 -0400 Subject: [PATCH 096/118] [Discover] Unskip Discover large field number test (#100692) (#103050) Co-authored-by: Matthias Wilhelm --- test/functional/apps/discover/_huge_fields.ts | 13 ++++------ .../es_archiver/huge_fields/data.json.gz | Bin 0 -> 49227 bytes .../es_archiver/huge_fields/mappings.json | 24 ++++++++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 test/functional/fixtures/es_archiver/huge_fields/data.json.gz create mode 100644 test/functional/fixtures/es_archiver/huge_fields/mappings.json diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts index c7fe0a94b6019..24b10e1df0495 100644 --- a/test/functional/apps/discover/_huge_fields.ts +++ b/test/functional/apps/discover/_huge_fields.ts @@ -15,21 +15,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/96113 - describe.skip('test large number of fields in sidebar', function () { + describe('test large number of fields in sidebar', function () { before(async function () { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/huge_fields'); await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields'); - await PageObjects.settings.navigateTo(); await kibanaServer.uiSettings.update({ 'timepicker:timeDefaults': `{ "from": "2016-10-05T00:00:00", "to": "2016-10-06T00:00:00"}`, }); - await PageObjects.settings.createIndexPattern('*huge*', 'date', true); await PageObjects.common.navigateToApp('discover'); }); it('test_huge data should have expected number of fields', async function () { - await PageObjects.discover.selectIndexPattern('*huge*'); + await PageObjects.discover.selectIndexPattern('testhuge*'); // initially this field should not be rendered const fieldExistsBeforeScrolling = await testSubjects.exists('field-myvar1050'); expect(fieldExistsBeforeScrolling).to.be(false); @@ -41,8 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); - await esArchiver.unload('test/functional/fixtures/es_archiver/large_fields'); - await kibanaServer.uiSettings.replace({}); + await esArchiver.unload('test/functional/fixtures/es_archiver/huge_fields'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); }); }); } diff --git a/test/functional/fixtures/es_archiver/huge_fields/data.json.gz b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..1ce42c64c53a34db899fedd40bc012fa2e6769ba GIT binary patch literal 49227 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 BgdP9@ literal 0 HcmV?d00001 diff --git a/test/functional/fixtures/es_archiver/huge_fields/mappings.json b/test/functional/fixtures/es_archiver/huge_fields/mappings.json new file mode 100644 index 0000000000000..49a677a42f2ba --- /dev/null +++ b/test/functional/fixtures/es_archiver/huge_fields/mappings.json @@ -0,0 +1,24 @@ +{ + "type": "index", + "value": { + "index": "testhuge", + "mappings": { + "properties": { + "date": { + "type": "date" + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "50000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "5" + } + } + } +} \ No newline at end of file From 2507c47992f03309c5e4a8619dcafaa367abdcd5 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 23 Jun 2021 12:30:55 +0200 Subject: [PATCH 097/118] Ingest pipeline locator (#102878) (#103053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 implement ingest pipeline locator * feat: 🎸 improve ingest pipeline locator * feat: 🎸 register ingest pipeline locator * refactor: 💡 use locator in expand_row component * chore: 🤖 remove ingest pipelines URL generator * fix: 🐛 correct TypeScript errors Co-authored-by: Vadim Kibana Co-authored-by: Vadim Kibana --- src/plugins/share/public/index.ts | 3 +- .../plugins/ingest_pipelines/public/index.ts | 7 -- .../ingest_pipelines/public/locator.test.ts | 100 ++++++++++++++++ .../ingest_pipelines/public/locator.ts | 102 +++++++++++++++++ .../plugins/ingest_pipelines/public/plugin.ts | 8 +- .../public/url_generator.test.ts | 108 ------------------ .../ingest_pipelines/public/url_generator.ts | 99 ---------------- .../models_management/expanded_row.tsx | 24 ++-- 8 files changed, 220 insertions(+), 231 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/locator.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/locator.ts delete mode 100644 x-pack/plugins/ingest_pipelines/public/url_generator.test.ts delete mode 100644 x-pack/plugins/ingest_pipelines/public/url_generator.ts diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 8f5356f6a2201..5ee3156534c5e 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -7,7 +7,8 @@ */ export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; -export { LocatorDefinition } from '../common/url_service'; + +export { LocatorDefinition, LocatorPublic, KibanaLocation } from '../common/url_service'; export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts index 8948a3e8d56be..d120f60ef8a2d 100644 --- a/x-pack/plugins/ingest_pipelines/public/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -10,10 +10,3 @@ import { IngestPipelinesPlugin } from './plugin'; export function plugin() { return new IngestPipelinesPlugin(); } - -export { - INGEST_PIPELINES_APP_ULR_GENERATOR, - IngestPipelinesUrlGenerator, - IngestPipelinesUrlGeneratorState, - INGEST_PIPELINES_PAGES, -} from './url_generator'; diff --git a/x-pack/plugins/ingest_pipelines/public/locator.test.ts b/x-pack/plugins/ingest_pipelines/public/locator.test.ts new file mode 100644 index 0000000000000..0b1246b2bed59 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/locator.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { ManagementAppLocatorDefinition } from 'src/plugins/management/common/locator'; +import { IngestPipelinesLocatorDefinition, INGEST_PIPELINES_PAGES } from './locator'; + +describe('Ingest pipeline locator', () => { + const setup = () => { + const managementDefinition = new ManagementAppLocatorDefinition(); + const definition = new IngestPipelinesLocatorDefinition({ + managementAppLocator: { + getLocation: (params) => managementDefinition.getLocation(params), + getUrl: async () => { + throw new Error('not implemented'); + }, + navigate: async () => { + throw new Error('not implemented'); + }, + useUrl: () => '', + }, + }); + return { definition }; + }; + + describe('Pipelines List', () => { + it('generates relative url for list without pipelineId', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.LIST, + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines', + }); + }); + + it('generates relative url for list with a pipelineId', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.LIST, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/?pipeline=pipeline_name', + }); + }); + }); + + describe('Pipeline Edit', () => { + it('generates relative url for pipeline edit', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.EDIT, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/edit/pipeline_name', + }); + }); + }); + + describe('Pipeline Clone', () => { + it('generates relative url for pipeline clone', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.CLONE, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/create/pipeline_name', + }); + }); + }); + + describe('Pipeline Create', () => { + it('generates relative url for pipeline create', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.CREATE, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/create', + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/locator.ts b/x-pack/plugins/ingest_pipelines/public/locator.ts new file mode 100644 index 0000000000000..d819011f14f47 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/locator.ts @@ -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 { SerializableState } from 'src/plugins/kibana_utils/common'; +import { ManagementAppLocator } from 'src/plugins/management/common'; +import { + LocatorPublic, + LocatorDefinition, + KibanaLocation, +} from '../../../../src/plugins/share/public'; +import { + getClonePath, + getCreatePath, + getEditPath, + getListPath, +} from './application/services/navigation'; +import { PLUGIN_ID } from '../common/constants'; + +export enum INGEST_PIPELINES_PAGES { + LIST = 'pipelines_list', + EDIT = 'pipeline_edit', + CREATE = 'pipeline_create', + CLONE = 'pipeline_clone', +} + +interface IngestPipelinesBaseParams extends SerializableState { + pipelineId: string; +} +export interface IngestPipelinesListParams extends Partial { + page: INGEST_PIPELINES_PAGES.LIST; +} + +export interface IngestPipelinesEditParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.EDIT; +} + +export interface IngestPipelinesCloneParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.CLONE; +} + +export interface IngestPipelinesCreateParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.CREATE; +} + +export type IngestPipelinesParams = + | IngestPipelinesListParams + | IngestPipelinesEditParams + | IngestPipelinesCloneParams + | IngestPipelinesCreateParams; + +export type IngestPipelinesLocator = LocatorPublic; + +export const INGEST_PIPELINES_APP_LOCATOR = 'INGEST_PIPELINES_APP_LOCATOR'; + +export interface IngestPipelinesLocatorDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class IngestPipelinesLocatorDefinition implements LocatorDefinition { + public readonly id = INGEST_PIPELINES_APP_LOCATOR; + + constructor(protected readonly deps: IngestPipelinesLocatorDependencies) {} + + public readonly getLocation = async (params: IngestPipelinesParams): Promise => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'ingest', + appId: PLUGIN_ID, + }); + + let path: string = ''; + + switch (params.page) { + case INGEST_PIPELINES_PAGES.EDIT: + path = getEditPath({ + pipelineName: params.pipelineId, + }); + break; + case INGEST_PIPELINES_PAGES.CREATE: + path = getCreatePath(); + break; + case INGEST_PIPELINES_PAGES.LIST: + path = getListPath({ + inspectedPipelineName: params.pipelineId, + }); + break; + case INGEST_PIPELINES_PAGES.CLONE: + path = getClonePath({ + clonedPipelineName: params.pipelineId, + }); + break; + } + + return { + ...location, + path: path === '/' ? location.path : location.path + path, + }; + }; +} diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 4a138a12d6819..b4eb33162a1f4 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -11,7 +11,7 @@ import { CoreSetup, Plugin } from 'src/core/public'; import { PLUGIN_ID } from '../common/constants'; import { uiMetricService, apiService } from './application/services'; import { SetupDependencies, StartDependencies } from './types'; -import { registerUrlGenerator } from './url_generator'; +import { IngestPipelinesLocatorDefinition } from './locator'; export class IngestPipelinesPlugin implements Plugin { @@ -50,7 +50,11 @@ export class IngestPipelinesPlugin }, }); - registerUrlGenerator(coreSetup, management, share); + share.url.locators.create( + new IngestPipelinesLocatorDefinition({ + managementAppLocator: management.locator, + }) + ); } public start() {} diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts deleted file mode 100644 index dc45f9bc39088..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts +++ /dev/null @@ -1,108 +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 { IngestPipelinesUrlGenerator, INGEST_PIPELINES_PAGES } from './url_generator'; - -describe('IngestPipelinesUrlGenerator', () => { - const getAppBasePath = (absolute: boolean = false) => { - if (absolute) { - return Promise.resolve('http://localhost/app/test_app'); - } - return Promise.resolve('/app/test_app'); - }; - const urlGenerator = new IngestPipelinesUrlGenerator(getAppBasePath); - - describe('Pipelines List', () => { - it('generates relative url for list without pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - }); - expect(url).toBe('/app/test_app/'); - }); - - it('generates absolute url for list without pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/'); - }); - it('generates relative url for list with a pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/?pipeline=pipeline_name'); - }); - - it('generates absolute url for list with a pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/?pipeline=pipeline_name'); - }); - }); - - describe('Pipeline Edit', () => { - it('generates relative url for pipeline edit', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.EDIT, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/edit/pipeline_name'); - }); - - it('generates absolute url for pipeline edit', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.EDIT, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/edit/pipeline_name'); - }); - }); - - describe('Pipeline Clone', () => { - it('generates relative url for pipeline clone', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CLONE, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/create/pipeline_name'); - }); - - it('generates absolute url for pipeline clone', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CLONE, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/create/pipeline_name'); - }); - }); - - describe('Pipeline Create', () => { - it('generates relative url for pipeline create', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CREATE, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/create'); - }); - - it('generates absolute url for pipeline create', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CREATE, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/create'); - }); - }); -}); diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.ts deleted file mode 100644 index d9a77addcd5fd..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/url_generator.ts +++ /dev/null @@ -1,99 +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 { CoreSetup } from 'src/core/public'; -import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; -import { - getClonePath, - getCreatePath, - getEditPath, - getListPath, -} from './application/services/navigation'; -import { SetupDependencies } from './types'; -import { PLUGIN_ID } from '../common/constants'; - -export const INGEST_PIPELINES_APP_ULR_GENERATOR = 'INGEST_PIPELINES_APP_URL_GENERATOR'; - -export enum INGEST_PIPELINES_PAGES { - LIST = 'pipelines_list', - EDIT = 'pipeline_edit', - CREATE = 'pipeline_create', - CLONE = 'pipeline_clone', -} - -interface UrlGeneratorState { - pipelineId: string; - absolute?: boolean; -} -export interface PipelinesListUrlGeneratorState extends Partial { - page: INGEST_PIPELINES_PAGES.LIST; -} - -export interface PipelineEditUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.EDIT; -} - -export interface PipelineCloneUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.CLONE; -} - -export interface PipelineCreateUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.CREATE; -} - -export type IngestPipelinesUrlGeneratorState = - | PipelinesListUrlGeneratorState - | PipelineEditUrlGeneratorState - | PipelineCloneUrlGeneratorState - | PipelineCreateUrlGeneratorState; - -export class IngestPipelinesUrlGenerator - implements UrlGeneratorsDefinition { - constructor(private readonly getAppBasePath: (absolute: boolean) => Promise) {} - - public readonly id = INGEST_PIPELINES_APP_ULR_GENERATOR; - - public readonly createUrl = async (state: IngestPipelinesUrlGeneratorState): Promise => { - switch (state.page) { - case INGEST_PIPELINES_PAGES.EDIT: { - return `${await this.getAppBasePath(!!state.absolute)}${getEditPath({ - pipelineName: state.pipelineId, - })}`; - } - case INGEST_PIPELINES_PAGES.CREATE: { - return `${await this.getAppBasePath(!!state.absolute)}${getCreatePath()}`; - } - case INGEST_PIPELINES_PAGES.LIST: { - return `${await this.getAppBasePath(!!state.absolute)}${getListPath({ - inspectedPipelineName: state.pipelineId, - })}`; - } - case INGEST_PIPELINES_PAGES.CLONE: { - return `${await this.getAppBasePath(!!state.absolute)}${getClonePath({ - clonedPipelineName: state.pipelineId, - })}`; - } - } - }; -} - -export const registerUrlGenerator = ( - coreSetup: CoreSetup, - management: SetupDependencies['management'], - share: SetupDependencies['share'] -) => { - const getAppBasePath = async (absolute = false) => { - const [coreStart] = await coreSetup.getStartServices(); - return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { - path: management.sections.section.ingest.getApp(PLUGIN_ID)!.basePath, - absolute: !!absolute, - }); - }; - - share.urlGenerators.registerUrlGenerator(new IngestPipelinesUrlGenerator(getAppBasePath)); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx index 88ffaa0da7fdc..93be45bbdaf97 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx @@ -114,10 +114,7 @@ export const ExpandedRow: FC = ({ item }) => { } const { - services: { - share, - application: { navigateToUrl }, - }, + services: { share }, } = useMlKibana(); const tabs = [ @@ -402,17 +399,16 @@ export const ExpandedRow: FC = ({ item }) => { { - const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator( - 'INGEST_PIPELINES_APP_URL_GENERATOR' - ); - await navigateToUrl( - await ingestPipelinesAppUrlGenerator.createUrl({ - page: 'pipeline_edit', - pipelineId: pipelineName, - absolute: true, - }) + onClick={() => { + const locator = share.url.locators.get( + 'INGEST_PIPELINES_APP_LOCATOR' ); + if (!locator) return; + locator.navigate({ + page: 'pipeline_edit', + pipelineId: pipelineName, + absolute: true, + }); }} > Date: Wed, 23 Jun 2021 06:31:51 -0400 Subject: [PATCH 098/118] [Lens] Avoid suggestion rendering and evaluation on fullscreen mode (#102757) (#103054) Co-authored-by: Marco Liberati --- .../editor_frame/editor_frame.test.tsx | 51 +++++++++++++++++++ .../editor_frame/editor_frame.tsx | 3 +- .../editor_frame/frame_layout.scss | 5 +- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 52488cb32ae83..0e2ba5ce8ad59 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -1370,6 +1370,57 @@ describe('editor_frame', () => { }) ); }); + + it('should avoid completely to compute suggestion when in fullscreen mode', async () => { + const props = { + ...getDefaultProps(), + initialContext: { + indexPatternId: '1', + fieldName: 'test', + }, + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + + const { instance: el } = await mountWithProvider( + , + props.plugins.data + ); + instance = el; + + expect( + instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement + ).not.toBeUndefined(); + + await act(async () => { + (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ + type: 'TOGGLE_FULLSCREEN', + }); + }); + + instance.update(); + + expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false); + + await act(async () => { + (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ + type: 'TOGGLE_FULLSCREEN', + }); + }); + + instance.update(); + + expect( + instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement + ).not.toBeUndefined(); + }); }); describe('passing state back to the caller', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index cc65bb126d2d9..bd96682f427fa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -452,7 +452,8 @@ export function EditorFrame(props: EditorFrameProps) { ) } suggestionsPanel={ - allLoaded && ( + allLoaded && + !state.isFullscreenDatasource && ( Date: Wed, 23 Jun 2021 06:33:13 -0400 Subject: [PATCH 099/118] [Lens] Remove rank direction tooltip (#102886) (#103052) Co-authored-by: Marco Liberati --- .../operations/definitions/terms/index.tsx | 22 +++---------------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 7551b88039182..a650c556c4965 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -424,25 +424,9 @@ export const termsOperation: OperationDefinition - {i18n.translate('xpack.lens.indexPattern.terms.orderDirection', { - defaultMessage: 'Rank direction', - })}{' '} - - - } + label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', { + defaultMessage: 'Rank direction', + })} display="columnCompressed" fullWidth > diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index efffd2b6d7b0f..b722682616843 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12743,7 +12743,6 @@ "xpack.lens.indexPattern.terms.orderByHelp": "上位の値がランク付けされる条件となるディメンションを指定します。", "xpack.lens.indexPattern.terms.orderDescending": "降順", "xpack.lens.indexPattern.terms.orderDirection": "ランク方向", - "xpack.lens.indexPattern.terms.orderDirectionHelp": "上位の値のランク順序を指定します。", "xpack.lens.indexPattern.terms.otherBucketDescription": "他の値を「その他」としてグループ化", "xpack.lens.indexPattern.terms.otherLabel": "その他", "xpack.lens.indexPattern.terms.size": "値の数", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d2916c4ef8704..7e16125888d8d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12914,7 +12914,6 @@ "xpack.lens.indexPattern.terms.orderByHelp": "指定排名靠前值排名所依据的维度。", "xpack.lens.indexPattern.terms.orderDescending": "降序", "xpack.lens.indexPattern.terms.orderDirection": "排名方向", - "xpack.lens.indexPattern.terms.orderDirectionHelp": "指定排名靠前值的排名顺序。", "xpack.lens.indexPattern.terms.otherBucketDescription": "将其他值分组为“其他”", "xpack.lens.indexPattern.terms.otherLabel": "其他", "xpack.lens.indexPattern.terms.size": "值数目", From 227e5bbb3c9f7c1cfab46abe75e1d0043eae8454 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 07:18:46 -0400 Subject: [PATCH 100/118] [OsQuery] fix usage collector when .fleet indices are empty (#102977) (#103058) Co-authored-by: Nicolas Chaulet --- x-pack/plugins/osquery/server/usage/fetchers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts index 5f5b282331bec..3142b72906898 100644 --- a/x-pack/plugins/osquery/server/usage/fetchers.ts +++ b/x-pack/plugins/osquery/server/usage/fetchers.ts @@ -55,6 +55,7 @@ export async function getPolicyLevelUsage( }, }, index: '.fleet-agents', + ignore_unavailable: true, }); const policied = agentResponse.body.aggregations?.policied as AggregationsSingleBucketAggregate; if (policied && typeof policied.doc_count === 'number') { @@ -117,6 +118,7 @@ export async function getLiveQueryUsage( }, }, index: '.fleet-actions', + ignore_unavailable: true, }); const result: LiveQueryUsage = { session: await getRouteMetric(soClient, 'live_query'), @@ -185,6 +187,7 @@ export async function getBeatUsage(esClient: ElasticsearchClient) { }, }, index: METRICS_INDICES, + ignore_unavailable: true, }); const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate; const result: BeatMetricsUsage = { From 8cf034bcbfcca202b0d1e78fc551ef9687b856c1 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 08:14:17 -0400 Subject: [PATCH 101/118] [cli] Add kibana-encryption-keys.bat (#102070) (#103065) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jonathan Budzenski --- .../bin/scripts/kibana-encryption-keys.bat | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat new file mode 100755 index 0000000000000..9221af3142e61 --- /dev/null +++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat @@ -0,0 +1,35 @@ +@echo off + +SETLOCAL ENABLEDELAYEDEXPANSION + +set SCRIPT_DIR=%~dp0 +for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI + +set NODE=%DIR%\node\node.exe + +If Not Exist "%NODE%" ( + Echo unable to find usable node.js executable. + Exit /B 1 +) + +set CONFIG_DIR=%KBN_PATH_CONF% +If [%KBN_PATH_CONF%] == [] ( + set "CONFIG_DIR=%DIR%\config" +) + +IF EXIST "%CONFIG_DIR%\node.options" ( + for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do ( + If [!NODE_OPTIONS!] == [] ( + set "NODE_OPTIONS=%%i" + ) Else ( + set "NODE_OPTIONS=!NODE_OPTIONS! %%i" + ) + ) +) + +TITLE Kibana Encryption Keys +"%NODE%" "%DIR%\src\cli_encryption_keys\dist" %* + +:finally + +ENDLOCAL From 9e2b89d4e7c07bab680878ee8c81da0d34aa82d4 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 23 Jun 2021 15:32:57 +0200 Subject: [PATCH 102/118] Discover locator (#102712) (#103072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Discover locator * Add Discover locator tests * Expose locator for Discover app and deprecate URL generator * Use Discover locator in Explore Underlying Data * Fix explore data unit tests after refactor * fix: 🐛 update Discover plugin mock * style: 💄 remove any * test: 💍 fix test mock * fix: 🐛 adjust property name after refactor * test: 💍 fix tests after refactor Co-authored-by: Vadim Kibana Co-authored-by: Vadim Kibana --- src/plugins/discover/public/index.ts | 2 + src/plugins/discover/public/locator.test.ts | 270 ++++++++++++++++++ src/plugins/discover/public/locator.ts | 146 ++++++++++ src/plugins/discover/public/mocks.ts | 12 + src/plugins/discover/public/plugin.tsx | 76 ++++- x-pack/plugins/discover_enhanced/kibana.json | 2 +- .../abstract_explore_data_action.ts | 22 +- .../explore_data_chart_action.test.ts | 46 ++- .../explore_data/explore_data_chart_action.ts | 28 +- .../explore_data_context_menu_action.test.ts | 42 ++- .../explore_data_context_menu_action.ts | 28 +- 11 files changed, 586 insertions(+), 88 deletions(-) create mode 100644 src/plugins/discover/public/locator.test.ts create mode 100644 src/plugins/discover/public/locator.ts diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index fbe853ec6deb5..3840df4353faf 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -17,4 +17,6 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; export { loadSharingDataHelpers } from './shared'; + export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; +export { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator'; diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts new file mode 100644 index 0000000000000..edbb0663d4aa3 --- /dev/null +++ b/src/plugins/discover/public/locator.test.ts @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public'; +import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; +import { FilterStateStore } from '../../data/common'; +import { DiscoverAppLocatorDefinition } from './locator'; +import { SerializableState } from 'src/plugins/kibana_utils/common'; + +const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; +const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; + +interface SetupParams { + useHash?: boolean; +} + +const setup = async ({ useHash = false }: SetupParams = {}) => { + const locator = new DiscoverAppLocatorDefinition({ + useHash, + }); + + return { + locator, + }; +}; + +beforeEach(() => { + // @ts-expect-error + hashedItemStore.storage = mockStorage; +}); + +describe('Discover url generator', () => { + test('can create a link to Discover with no state and no saved search', async () => { + const { locator } = await setup(); + const { app, path } = await locator.getLocation({}); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(app).toBe('discover'); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can create a link to a saved search in Discover', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ savedSearchId }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(path.startsWith(`#/view/${savedSearchId}`)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can specify specific index pattern', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + indexPatternId, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + index: indexPatternId, + }); + expect(_g).toEqual({}); + }); + + test('can specify specific time range', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }); + }); + + test('can specify query', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + expect(_g).toEqual({}); + }); + + test('can specify local and global filters', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + filters: [ + { + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + { + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + }, + ], + }); + expect(_g).toEqual({ + filters: [ + { + $state: { + store: 'globalState', + }, + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + }, + ], + }); + }); + + test('can set refresh interval', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + }); + + test('can set time range', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + timeRange: { + from: 'now-3h', + to: 'now', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-3h', + to: 'now', + }, + }); + }); + + test('can specify a search session id', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + searchSessionId: '__test__', + }); + + expect(path).toMatchInlineSnapshot(`"#/?_g=()&_a=()&searchSessionId=__test__"`); + expect(path).toContain('__test__'); + }); + + test('can specify columns, interval, sort and savedQuery', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + columns: ['_source'], + interval: 'auto', + sort: [['timestamp, asc']] as string[][] & SerializableState, + savedQuery: '__savedQueryId__', + }); + + expect(path).toMatchInlineSnapshot( + `"#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"` + ); + }); + + describe('useHash property', () => { + describe('when default useHash is set to false', () => { + test('when using default, sets index pattern ID in the generated URL', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(true); + }); + + test('when enabling useHash, does not set index pattern ID in the generated URL', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + useHash: true, + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(false); + }); + }); + + describe('when default useHash is set to true', () => { + test('when using default, does not set index pattern ID in the generated URL', async () => { + const { locator } = await setup({ useHash: true }); + const { path } = await locator.getLocation({ + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(false); + }); + + test('when disabling useHash, sets index pattern ID in the generated URL', async () => { + const { locator } = await setup({ useHash: true }); + const { path } = await locator.getLocation({ + useHash: false, + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(true); + }); + }); + }); +}); diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts new file mode 100644 index 0000000000000..fff89903bc465 --- /dev/null +++ b/src/plugins/discover/public/locator.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import type { LocatorDefinition, LocatorPublic } from '../../share/public'; +import { esFilters } from '../../data/public'; +import { setStateToKbnUrl } from '../../kibana_utils/public'; + +export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; + +export interface DiscoverAppLocatorParams extends SerializableState { + /** + * Optionally set saved search ID. + */ + savedSearchId?: string; + + /** + * Optionally set index pattern ID. + */ + indexPatternId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval & SerializableState; + + /** + * Optionally apply filters. + */ + filters?: Filter[]; + + /** + * Optionally set a query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; + + /** + * Background search session id + */ + searchSessionId?: string; + + /** + * Columns displayed in the table + */ + columns?: string[]; + + /** + * Used interval of the histogram + */ + interval?: string; + + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][] & SerializableState; + + /** + * id of the used saved query + */ + savedQuery?: string; +} + +export type DiscoverAppLocator = LocatorPublic; + +export interface DiscoverAppLocatorDependencies { + useHash: boolean; +} + +export class DiscoverAppLocatorDefinition implements LocatorDefinition { + public readonly id = DISCOVER_APP_LOCATOR; + + constructor(protected readonly deps: DiscoverAppLocatorDependencies) {} + + public readonly getLocation = async (params: DiscoverAppLocatorParams) => { + const { + useHash = this.deps.useHash, + filters, + indexPatternId, + query, + refreshInterval, + savedSearchId, + timeRange, + searchSessionId, + columns, + savedQuery, + sort, + interval, + } = params; + const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : ''; + const appState: { + query?: Query; + filters?: Filter[]; + index?: string; + columns?: string[]; + interval?: string; + sort?: string[][]; + savedQuery?: string; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (indexPatternId) appState.index = indexPatternId; + if (columns) appState.columns = columns; + if (savedQuery) appState.savedQuery = savedQuery; + if (sort) appState.sort = sort; + if (interval) appState.interval = interval; + + if (timeRange) queryState.time = timeRange; + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let path = `#/${savedSearchPath}`; + path = setStateToKbnUrl('_g', queryState, { useHash }, path); + path = setStateToKbnUrl('_a', appState, { useHash }, path); + + if (searchSessionId) { + path = `${path}&searchSessionId=${searchSessionId}`; + } + + return { + app: 'discover', + path, + state: {}, + }; + }; +} diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index 0f57c5c0fa138..53160df472a3c 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -16,6 +16,12 @@ const createSetupContract = (): Setup => { docViews: { addDocView: jest.fn(), }, + locator: { + getLocation: jest.fn(), + getUrl: jest.fn(), + useUrl: jest.fn(), + navigate: jest.fn(), + }, }; return setupContract; }; @@ -26,6 +32,12 @@ const createStartContract = (): Start => { urlGenerator: ({ createUrl: jest.fn(), } as unknown) as DiscoverStart['urlGenerator'], + locator: { + getLocation: jest.fn(), + getUrl: jest.fn(), + useUrl: jest.fn(), + navigate: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 7b4e7bb67c00e..ec89f7516e92d 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -59,6 +59,7 @@ import { DiscoverUrlGenerator, SEARCH_SESSION_ID_QUERY_PARAM, } from './url_generator'; +import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator'; import { SearchEmbeddableFactory } from './application/embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { replaceUrlHashQuery } from '../../kibana_utils/public/'; @@ -83,17 +84,68 @@ export interface DiscoverSetup { */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; }; + + /** + * `share` plugin URL locator for Discover app. Use it to generate links into + * Discover application, for example, navigate: + * + * ```ts + * await plugins.discover.locator.navigate({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + * + * Generate a location: + * + * ```ts + * const location = await plugins.discover.locator.getLocation({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + */ + readonly locator: undefined | DiscoverAppLocator; } export interface DiscoverStart { savedSearchLoader: SavedObjectLoader; /** - * `share` plugin URL generator for Discover app. Use it to generate links into - * Discover application, example: + * @deprecated Use URL locator instead. URL generaotr will be removed. + */ + readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + + /** + * `share` plugin URL locator for Discover app. Use it to generate links into + * Discover application, for example, navigate: + * + * ```ts + * await plugins.discover.locator.navigate({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + * + * Generate a location: * * ```ts - * const url = await plugins.discover.urlGenerator.createUrl({ + * const location = await plugins.discover.locator.getLocation({ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', * timeRange: { @@ -104,7 +156,7 @@ export interface DiscoverStart { * }); * ``` */ - readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + readonly locator: undefined | DiscoverAppLocator; } /** @@ -156,7 +208,12 @@ export class DiscoverPlugin private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; private innerAngularInitialized: boolean = false; + + /** + * @deprecated + */ private urlGenerator?: DiscoverStart['urlGenerator']; + private locator?: DiscoverAppLocator; /** * why are those functions public? they are needed for some mocha tests @@ -179,6 +236,15 @@ export class DiscoverPlugin }) ); } + + if (plugins.share) { + this.locator = plugins.share.url.locators.create( + new DiscoverAppLocatorDefinition({ + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + } + this.docViewsRegistry = new DocViewsRegistry(); setDocViewsRegistry(this.docViewsRegistry); this.docViewsRegistry.addDocView({ @@ -323,6 +389,7 @@ export class DiscoverPlugin docViews: { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }, + locator: this.locator, }; } @@ -367,6 +434,7 @@ export class DiscoverPlugin return { urlGenerator: this.urlGenerator, + locator: this.locator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json index 01a3624d3e320..da95a0f21a020 100644 --- a/x-pack/plugins/discover_enhanced/kibana.json +++ b/x-pack/plugins/discover_enhanced/kibana.json @@ -7,5 +7,5 @@ "requiredPlugins": ["uiActions", "embeddable", "discover"], "optionalPlugins": ["share", "kibanaLegacy", "usageCollection"], "configPath": ["xpack", "discoverEnhanced"], - "requiredBundles": ["kibanaUtils", "data", "share"] + "requiredBundles": ["kibanaUtils", "data"] } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 023db127ca633..44ea53fe0b870 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -11,13 +11,13 @@ import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/ import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public'; import { CoreStart } from '../../../../../../src/core/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; export interface PluginDeps { - discover: Pick; + discover: Pick; kibanaLegacy?: { dashboardConfig: { getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls']; @@ -26,7 +26,7 @@ export interface PluginDeps { } export interface CoreDeps { - application: Pick; + application: Pick; } export interface Params { @@ -43,7 +43,7 @@ export abstract class AbstractExploreDataAction; + protected abstract getLocation(context: Context): Promise; public async isCompatible({ embeddable }: Context): Promise { if (!embeddable) return false; @@ -52,7 +52,7 @@ export abstract class AbstractExploreDataAction { - type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; - const core = coreMock.createStart(); - - const urlGenerator: UrlGenerator = ({ - createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), - } as unknown) as UrlGenerator; + const locator: DiscoverAppLocator = { + getLocation: jest.fn(() => + Promise.resolve({ + app: 'discover', + path: '/foo#bar', + state: {}, + }) + ), + navigate: jest.fn(async () => {}), + getUrl: jest.fn(), + useUrl: jest.fn(), + }; const plugins: PluginDeps = { discover: { - urlGenerator, + locator, }, kibanaLegacy: { dashboardConfig: { @@ -95,7 +101,7 @@ const setup = ( embeddable, } as ExploreDataChartActionContext; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; + return { core, plugins, locator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -132,7 +138,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false when URL generator is not present', async () => { const { action, plugins, context } = setup(); - (plugins.discover as any).urlGenerator = undefined; + (plugins.discover as any).locator = undefined; const isCompatible = await action.isCompatible(context); @@ -205,23 +211,15 @@ describe('"Explore underlying data" panel action', () => { }); describe('getHref()', () => { - test('returns URL path generated by URL generator', async () => { - const { action, context } = setup(); - - const href = await action.getHref(context); - - expect(href).toBe('/xyz/app/discover/foo#bar'); - }); - test('calls URL generator with right arguments', async () => { - const { action, urlGenerator, context } = setup(); + const { action, locator, context } = setup(); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + expect(locator.getLocation).toHaveBeenCalledTimes(0); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledTimes(1); + expect(locator.getLocation).toHaveBeenCalledWith({ filters: [], indexPatternId: 'index-ptr-foo', timeRange: undefined, @@ -260,11 +258,11 @@ describe('"Explore underlying data" panel action', () => { }, ]; - const { action, context, urlGenerator } = setup({ filters, timeFieldName }); + const { action, context, locator } = setup({ filters, timeFieldName }); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledWith({ filters: [ { meta: { diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 32264ee1deceb..7b59a4e51d042 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -7,7 +7,7 @@ import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { - DiscoverUrlGeneratorState, + DiscoverAppLocatorParams, SearchInput, } from '../../../../../../src/plugins/discover/public'; import { @@ -15,7 +15,7 @@ import { esFilters, } from '../../../../../../src/plugins/data/public'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; @@ -43,14 +43,14 @@ export class ExploreDataChartAction return super.isCompatible(context); } - protected readonly getUrl = async ( + protected readonly getLocation = async ( context: ExploreDataChartActionContext - ): Promise => { + ): Promise => { const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; + const { locator } = plugins.discover; - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); + if (!locator) { + throw new Error('Discover URL locator not available.'); } const { embeddable } = context; @@ -59,23 +59,23 @@ export class ExploreDataChartAction context.timeFieldName ); - const state: DiscoverUrlGeneratorState = { + const params: DiscoverAppLocatorParams = { filters, timeRange, }; if (embeddable) { - state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; + params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; const input = embeddable.getInput() as Readonly; - if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange; - if (input.query) state.query = input.query; - if (input.filters) state.filters = [...input.filters, ...(state.filters || [])]; + if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange; + if (input.query) params.query = input.query; + if (input.filters) params.filters = [...input.filters, ...(params.filters || [])]; } - const path = await urlGenerator.createUrl(state); + const location = await locator.getLocation(params); - return new KibanaURL(path); + return location; }; } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index 842c7d6b339b4..5bdac602ec271 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -8,13 +8,13 @@ import { ExploreDataContextMenuAction } from './explore_data_context_menu_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; -import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, VISUALIZE_EMBEDDABLE_TYPE, } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverAppLocator } from '../../../../../../src/plugins/discover/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -29,17 +29,23 @@ afterEach(() => { }); const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = {}) => { - type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; - const core = coreMock.createStart(); - - const urlGenerator: UrlGenerator = ({ - createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), - } as unknown) as UrlGenerator; + const locator: DiscoverAppLocator = { + getLocation: jest.fn(() => + Promise.resolve({ + app: 'discover', + path: '/foo#bar', + state: {}, + }) + ), + navigate: jest.fn(async () => {}), + getUrl: jest.fn(), + useUrl: jest.fn(), + }; const plugins: PluginDeps = { discover: { - urlGenerator, + locator, }, kibanaLegacy: { dashboardConfig: { @@ -79,7 +85,7 @@ const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = embeddable, }; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; + return { core, plugins, locator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -116,7 +122,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false when URL generator is not present', async () => { const { action, plugins, context } = setup(); - (plugins.discover as any).urlGenerator = undefined; + (plugins.discover as any).locator = undefined; const isCompatible = await action.isCompatible(context); @@ -189,23 +195,15 @@ describe('"Explore underlying data" panel action', () => { }); describe('getHref()', () => { - test('returns URL path generated by URL generator', async () => { - const { action, context } = setup(); - - const href = await action.getHref(context); - - expect(href).toBe('/xyz/app/discover/foo#bar'); - }); - test('calls URL generator with right arguments', async () => { - const { action, urlGenerator, context } = setup(); + const { action, locator, context } = setup(); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + expect(locator.getLocation).toHaveBeenCalledTimes(0); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledTimes(1); + expect(locator.getLocation).toHaveBeenCalledWith({ indexPatternId: 'index-ptr-foo', }); }); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts index 99a2afd239645..88c093a299cb9 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -12,8 +12,8 @@ import { IEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public'; -import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { DiscoverAppLocatorParams } from '../../../../../../src/plugins/discover/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; @@ -40,29 +40,31 @@ export class ExploreDataContextMenuAction public readonly order = 200; - protected readonly getUrl = async (context: EmbeddableQueryContext): Promise => { + protected readonly getLocation = async ( + context: EmbeddableQueryContext + ): Promise => { const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; + const { locator } = plugins.discover; - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); + if (!locator) { + throw new Error('Discover URL locator not available.'); } const { embeddable } = context; - const state: DiscoverUrlGeneratorState = {}; + const params: DiscoverAppLocatorParams = {}; if (embeddable) { - state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; + params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; const input = embeddable.getInput(); - if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange; - if (input.query) state.query = input.query; - if (input.filters) state.filters = [...input.filters, ...(state.filters || [])]; + if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange; + if (input.query) params.query = input.query; + if (input.filters) params.filters = [...input.filters, ...(params.filters || [])]; } - const path = await urlGenerator.createUrl(state); + const location = await locator.getLocation(params); - return new KibanaURL(path); + return location; }; } From 03b880082c5d4759d387d971648be974b6c5ad81 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:49:21 -0400 Subject: [PATCH 103/118] fix time shift ux issues (#102709) (#103074) Co-authored-by: Joe Reuter --- .../search/aggs/utils/parse_time_shift.ts | 2 +- .../dimension_panel/time_shift.tsx | 2 +- .../time_shift_utils.tsx | 22 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts index 4d8ee0f889173..91379ea054de3 100644 --- a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts +++ b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts @@ -20,7 +20,7 @@ export const parseTimeShift = (val: string): moment.Duration | 'previous' | 'inv if (trimmedVal === 'previous') { return 'previous'; } - const [, amount, unit] = trimmedVal.match(/^(\d+)(\w)$/) || []; + const [, amount, unit] = trimmedVal.match(/^(\d+)\s*(\w)$/) || []; const parsedAmount = Number(amount); if (Number.isNaN(parsedAmount) || !allowedUnits.includes(unit as AllowedUnit)) { return 'invalid'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx index ba9525ac53fc5..c2415c9c9a75a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx @@ -157,7 +157,7 @@ export function TimeShift({ isClearable={false} data-test-subj="indexPattern-dimension-time-shift" placeholder={i18n.translate('xpack.lens.indexPattern.timeShiftPlaceholder', { - defaultMessage: 'Time shift (e.g. 1d)', + defaultMessage: 'Type custom values (e.g. 8w)', })} options={timeShiftOptions.filter(({ value }) => { const parsedValue = parseTimeShift(value); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index 14ba6b9189e6b..a1bc643c3bd93 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -23,67 +23,67 @@ import { FramePublicAPI } from '../types'; export const timeShiftOptions = [ { label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', { - defaultMessage: '1 hour (1h)', + defaultMessage: '1 hour ago (1h)', }), value: '1h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.3hours', { - defaultMessage: '3 hours (3h)', + defaultMessage: '3 hours ago (3h)', }), value: '3h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.6hours', { - defaultMessage: '6 hours (6h)', + defaultMessage: '6 hours ago (6h)', }), value: '6h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.12hours', { - defaultMessage: '12 hours (12h)', + defaultMessage: '12 hours ago (12h)', }), value: '12h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.day', { - defaultMessage: '1 day (1d)', + defaultMessage: '1 day ago (1d)', }), value: '1d', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.week', { - defaultMessage: '1 week (1w)', + defaultMessage: '1 week ago (1w)', }), value: '1w', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.month', { - defaultMessage: '1 month (1M)', + defaultMessage: '1 month ago (1M)', }), value: '1M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.3months', { - defaultMessage: '3 months (3M)', + defaultMessage: '3 months ago (3M)', }), value: '3M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.6months', { - defaultMessage: '6 months (6M)', + defaultMessage: '6 months ago (6M)', }), value: '6M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.year', { - defaultMessage: '1 year (1y)', + defaultMessage: '1 year ago (1y)', }), value: '1y', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { - defaultMessage: 'Previous', + defaultMessage: 'Previous time range', }), value: 'previous', }, From e583c6d358a56305182d8cdbd268e5b47dde0a1d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:52:10 -0400 Subject: [PATCH 104/118] disable missing switch for non-string fields (#102865) (#103073) Co-authored-by: Joe Reuter --- .../operations/definitions/terms/index.tsx | 5 ++- .../definitions/terms/terms.test.tsx | 32 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index a650c556c4965..a458a1edcfa16 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -497,7 +497,10 @@ export const termsOperation: OperationDefinition diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 3b557461546ca..f326f3e3ed5f6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -60,7 +60,7 @@ describe('terms', () => { size: 3, orderDirection: 'asc', }, - sourceField: 'category', + sourceField: 'source', }, col2: { label: 'Count', @@ -88,7 +88,7 @@ describe('terms', () => { expect.objectContaining({ arguments: expect.objectContaining({ orderBy: ['_key'], - field: ['category'], + field: ['source'], size: [3], otherBucket: [true], }), @@ -770,6 +770,34 @@ describe('terms', () => { expect(select.prop('disabled')).toEqual(false); }); + it('should disable missing bucket setting if field is not a string', () => { + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + + const select = instance + .find('[data-test-subj="indexPattern-terms-missing-bucket"]') + .find(EuiSwitch); + + expect(select.prop('disabled')).toEqual(true); + }); + it('should update state when clicking other bucket toggle', () => { const updateLayerSpy = jest.fn(); const instance = shallow( From 797c29c2d5f757c03ce5a95b36303dd0fc9990a2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:53:02 -0400 Subject: [PATCH 105/118] [Lens] Do not add math columns for pass-through operations (#102656) (#103075) Co-authored-by: Joe Reuter --- .../definitions/calculations/utils.ts | 23 ++++++- .../definitions/formula/formula.test.tsx | 20 ++---- .../operations/definitions/formula/parse.ts | 52 ++++++++------- .../operations/layer_helpers.test.ts | 63 +++++++++---------- 4 files changed, 85 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index 03b9d6c07709c..87116f71919b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -7,11 +7,12 @@ import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionAST } from '@kbn/interpreter/common'; +import memoizeOne from 'memoize-one'; import type { TimeScaleUnit } from '../../../time_scale'; import type { IndexPattern, IndexPatternLayer } from '../../../types'; import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils'; import type { ReferenceBasedIndexPatternColumn } from '../column_types'; -import { isColumnValidAsReference } from '../../layer_helpers'; +import { getManagedColumnsFrom, isColumnValidAsReference } from '../../layer_helpers'; import { operationDefinitionMap } from '..'; export const buildLabelFunction = (ofName: (name?: string) => string) => ( @@ -45,6 +46,23 @@ export function checkForDateHistogram(layer: IndexPatternLayer, name: string) { ]; } +const getFullyManagedColumnIds = memoizeOne((layer: IndexPatternLayer) => { + const managedColumnIds = new Set(); + Object.entries(layer.columns).forEach(([id, column]) => { + if ( + 'references' in column && + operationDefinitionMap[column.operationType].input === 'managedReference' + ) { + managedColumnIds.add(id); + const managedColumns = getManagedColumnsFrom(id, layer.columns); + managedColumns.map(([managedId]) => { + managedColumnIds.add(managedId); + }); + } + }); + return managedColumnIds; +}); + export function checkReferences(layer: IndexPatternLayer, columnId: string) { const column = layer.columns[columnId] as ReferenceBasedIndexPatternColumn; @@ -72,7 +90,8 @@ export function checkReferences(layer: IndexPatternLayer, columnId: string) { column: referenceColumn, }); - if (!isValid) { + // do not enforce column validity if current column is part of managed subtree + if (!isValid && !getFullyManagedColumnIds(layer).has(columnId)) { errors.push( i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', { defaultMessage: 'Dimension "{dimensionLabel}" is configured incorrectly', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index e6aa29ea4d763..279e76b839548 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -413,13 +413,13 @@ describe('formula', () => { ).newLayer ).toEqual({ ...layer, - columnOrder: ['col1X0', 'col1X1', 'col1'], + columnOrder: ['col1X0', 'col1'], columns: { ...layer.columns, col1: { ...currentColumn, label: 'average(bytes)', - references: ['col1X1'], + references: ['col1X0'], params: { ...currentColumn.params, formula: 'average(bytes)', @@ -436,18 +436,6 @@ describe('formula', () => { sourceField: 'bytes', timeScale: false, }, - col1X1: { - customLabel: true, - dataType: 'number', - isBucketed: false, - label: 'Part of average(bytes)', - operationType: 'math', - params: { - tinymathAst: 'col1X0', - }, - references: ['col1X0'], - scale: 'ratio', - }, }, }); }); @@ -568,8 +556,8 @@ describe('formula', () => { ).locations ).toEqual({ col1X0: { min: 15, max: 29 }, - col1X2: { min: 0, max: 41 }, - col1X3: { min: 42, max: 50 }, + col1X1: { min: 0, max: 41 }, + col1X2: { min: 42, max: 50 }, }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index 8b726d06f4602..cb1d0dc143efc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -123,17 +123,20 @@ function extractColumns( if (nodeOperation.input === 'fullReference') { const [referencedOp] = functions; const consumedParam = parseNode(referencedOp); + const hasActualMathContent = typeof consumedParam !== 'string'; - const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; - const mathColumn = mathOperation.buildColumn({ - layer, - indexPattern, - }); - mathColumn.references = subNodeVariables.map(({ value }) => value); - mathColumn.params.tinymathAst = consumedParam!; - columns.push({ column: mathColumn }); - mathColumn.customLabel = true; - mathColumn.label = label; + if (hasActualMathContent) { + const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = subNodeVariables.map(({ value }) => value); + mathColumn.params.tinymathAst = consumedParam!; + columns.push({ column: mathColumn }); + mathColumn.customLabel = true; + mathColumn.label = label; + } const mappedParams = getOperationParams(nodeOperation, namedArguments || []); const newCol = (nodeOperation as OperationDefinition< @@ -143,7 +146,11 @@ function extractColumns( { layer, indexPattern, - referenceIds: [getManagedId(idPrefix, columns.length - 1)], + referenceIds: [ + hasActualMathContent + ? getManagedId(idPrefix, columns.length - 1) + : (consumedParam as string), + ], }, mappedParams ); @@ -160,16 +167,19 @@ function extractColumns( if (root === undefined) { return []; } - const variables = findVariables(root); - const mathColumn = mathOperation.buildColumn({ - layer, - indexPattern, - }); - mathColumn.references = variables.map(({ value }) => value); - mathColumn.params.tinymathAst = root!; - mathColumn.customLabel = true; - mathColumn.label = label; - columns.push({ column: mathColumn }); + const topLevelMath = typeof root !== 'string'; + if (topLevelMath) { + const variables = findVariables(root); + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = variables.map(({ value }) => value); + mathColumn.params.tinymathAst = root!; + mathColumn.customLabel = true; + mathColumn.label = label; + columns.push({ column: mathColumn }); + } return columns; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 387a61ff79264..7de1318cbac61 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -25,6 +25,7 @@ import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; import { createMockedFullReference, createMockedManagedReference } from './mocks'; +import { TinymathAST } from 'packages/kbn-tinymath'; jest.mock('../operations'); jest.mock('../../id_generator'); @@ -105,28 +106,34 @@ describe('state_helpers', () => { const source = { dataType: 'number' as const, isBucketed: false, - label: 'moving_average(sum(bytes), window=5)', + label: '5 + moving_average(sum(bytes), window=5)', operationType: 'formula' as const, params: { - formula: 'moving_average(sum(bytes), window=5)', + formula: '5 + moving_average(sum(bytes), window=5)', isFormulaBroken: false, }, - references: ['formulaX1'], + references: ['formulaX2'], }; const math = { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', operationType: 'math' as const, - params: { tinymathAst: 'formulaX2' }, - references: ['formulaX2'], + label: 'Part of 5 + moving_average(sum(bytes), window=5)', + references: ['formulaX1'], + params: { + tinymathAst: { + type: 'function', + name: 'add', + args: [5, 'formulaX1'], + } as TinymathAST, + }, }; const sum = { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', + label: 'Part of 5 + moving_average(sum(bytes), window=5)', operationType: 'sum' as const, scale: 'ratio' as const, sourceField: 'bytes', @@ -135,7 +142,7 @@ describe('state_helpers', () => { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', + label: 'Part of 5 + moving_average(sum(bytes), window=5)', operationType: 'moving_average' as const, params: { window: 5 }, references: ['formulaX0'], @@ -148,14 +155,8 @@ describe('state_helpers', () => { columns: { source, formulaX0: sum, - formulaX1: math, - formulaX2: movingAvg, - formulaX3: { - ...math, - label: 'Part of moving_average(sum(bytes), window=5)', - references: ['formulaX2'], - params: { tinymathAst: 'formulaX2' }, - }, + formulaX1: movingAvg, + formulaX2: math, }, }, targetId: 'copy', @@ -171,40 +172,34 @@ describe('state_helpers', () => { 'formulaX0', 'formulaX1', 'formulaX2', - 'formulaX3', 'copyX0', 'copyX1', 'copyX2', - 'copyX3', 'copy', ], columns: { source, formulaX0: sum, - formulaX1: math, - formulaX2: movingAvg, - formulaX3: { - ...math, - references: ['formulaX2'], - params: { tinymathAst: 'formulaX2' }, - }, - copy: expect.objectContaining({ ...source, references: ['copyX3'] }), + formulaX1: movingAvg, + formulaX2: math, + copy: expect.objectContaining({ ...source, references: ['copyX2'] }), copyX0: expect.objectContaining({ ...sum, }), copyX1: expect.objectContaining({ - ...math, + ...movingAvg, references: ['copyX0'], - params: { tinymathAst: 'copyX0' }, }), copyX2: expect.objectContaining({ - ...movingAvg, - references: ['copyX1'], - }), - copyX3: expect.objectContaining({ ...math, - references: ['copyX2'], - params: { tinymathAst: 'copyX2' }, + references: ['copyX1'], + params: { + tinymathAst: expect.objectContaining({ + type: 'function', + name: 'add', + args: [5, 'copyX1'], + } as TinymathAST), + }, }), }, }); From 77d269fcc33633cc9492daef0a9709eea36946f9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:06:10 -0400 Subject: [PATCH 106/118] Update chart reference docs (#102430) (#103093) * Update chart reference docs * Update from feedback * Update from review feedback * Update more from comments * Apply left alignment Co-authored-by: Wylie Conlon --- .../dashboard/aggregation-reference.asciidoc | 448 +++++++++++------- docs/user/dashboard/lens.asciidoc | 36 ++ 2 files changed, 309 insertions(+), 175 deletions(-) diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc index cb5c484def3b9..17bfc19c2e0c9 100644 --- a/docs/user/dashboard/aggregation-reference.asciidoc +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -12,91 +12,168 @@ This reference can help simplify the comparison if you need a specific feature. [options="header"] |=== -| Type | Aggregation-based | Lens | TSVB | Timelion | Vega +| Type | Lens | TSVB | Agg-based | Vega | Timelion | Table -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | | -| Table with summary row -^| X -^| X -| +| Bar, line, and area +| ✓ +| ✓ +| ✓ +| ✓ +| ✓ + +| Split chart/small multiples | +| ✓ +| ✓ +| ✓ | -| Bar, line, and area charts -^| X -^| X -^| X -^| X -^| X +| Pie and donut +| ✓ +| +| ✓ +| ✓ +| -| Percentage bar or area chart +| Sunburst +| ✓ | -^| X -^| X +| ✓ +| ✓ | -^| X -| Split bar, line, and area charts -^| X +| Treemap +| ✓ +| | +| ✓ | + +| Heat map +| ✓ +| ✓ +| ✓ +| ✓ | -^| X -| Pie and donut charts -^| X -^| X +| Gauge and Goal | +| ✓ +| ✓ +| ✓ | -^| X -| Sunburst chart -^| X -^| X +| Markdown +| +| ✓ | | | -| Heat map -^| X -^| X +| Metric +| ✓ +| ✓ +| ✓ +| ✓ +| + +| Tag cloud | | -^| X +| ✓ +| ✓ +| -| Gauge and Goal -^| X +|=== + +[float] +[[table-features]] +=== Table features + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based + +| Summary row +| ✓ | -^| X +| ✓ + +| Pivot table +| ✓ | | -| Markdown +| Calculated column +| Formula +| ✓ +| Percent only + +| Color by value +| ✓ +| ✓ | + +|=== + +[float] +[[xy-features]] +=== Bar, line, area features + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based | Vega | Timelion + +| Dense time series +| Customizable +| ✓ +| Customizable +| ✓ +| ✓ + +| Percentage mode +| ✓ +| ✓ +| ✓ +| ✓ | -^| X + +| Break downs +| 1 +| 1 +| 3 +| ∞ +| 1 + +| Custom color with break downs | +| Only for Filters +| ✓ +| ✓ | -| Metric -^| X -^| X -^| X +| Fit missing values +| ✓ | -^| X +| ✓ +| ✓ +| ✓ -| Tag cloud -^| X +| Synchronized tooltips +| +| ✓ | | | -^| X |=== @@ -111,67 +188,57 @@ For information about {es} bucket aggregations, refer to {ref}/search-aggregatio [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Histogram -^| X -^| X -^| X +| ✓ | +| ✓ | Date histogram -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Date range -^| X -^| X -| +| Use filters | +| ✓ | Filter -^| X -^| X | -^| X +| ✓ +| | Filters -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | GeoHash grid -^| X -^| X | | +| ✓ | IP range -^| X -^| X -| -| +| Use filters +| Use filters +| ✓ | Range -^| X -^| X -^| X -| +| ✓ +| Use filters +| ✓ | Terms -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Significant terms -^| X -^| X | -^| X +| +| ✓ |=== @@ -186,67 +253,57 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Metrics with filters +| ✓ | | -^| X -| - -| Average -^| X -^| X -^| X -^| X -| Sum -^| X -^| X -^| X -^| X +| Average, Sum, Max, Min +| ✓ +| ✓ +| ✓ | Unique count (Cardinality) -^| X -^| X -^| X -^| X - -| Max -^| X -^| X -^| X -^| X - -| Min -^| X -^| X -^| X -^| X - -| Percentiles -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ + +| Percentiles and Median +| ✓ +| ✓ +| ✓ | Percentiles Rank -^| X -^| X -| -^| X +| +| ✓ +| ✓ + +| Standard deviation +| +| ✓ +| ✓ + +| Sum of squares +| +| ✓ +| | Top hit (Last value) -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Value count | | +| ✓ + +| Variance +| +| ✓ | -^| X |=== @@ -261,61 +318,94 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Avg bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Derivative -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Max bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Min bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Sum bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Moving average -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Cumulative sum -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Bucket script | | +| ✓ + +| Bucket selector +| | -^| X +| | Serial differencing -^| X -^| X | -^| X +| ✓ +| ✓ + +|=== + +[float] +[[custom-functions]] +=== Additional functions + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based + +| Counter rate +| ✓ +| ✓ +| + +| <> +| Use <> +| ✓ +| + +| <> +| +| ✓ +| + +| <> +| +| ✓ +| + +| Static value +| +| ✓ +| + |=== @@ -329,41 +419,49 @@ build their advanced visualization. [options="header"] |=== -| Type | Agg-based | Lens | TSVB | Timelion | Vega +| Type | Lens | TSVB | Agg-based | Vega | Timelion -| Math on aggregated data +| Math +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Visualize two indices +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Math across indices | | | -^| X -^| X +| ✓ +| ✓ | Time shifts +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Fully custom {es} queries | | | +| ✓ | -^| X + +| Normalize by time +| ✓ +| ✓ +| +| +| + |=== diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 4ecfcc9250122..2071f17ecff3d 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -139,6 +139,42 @@ image::images/lens_drag_drop_3.gif[Using drag and drop to reorder] . Press Space bar to confirm, or to cancel, press Esc. +[float] +[[lens-formulas]] +==== Use formulas to perform math + +Formulas let you perform math on aggregated data in Lens by typing +math and quick functions. To access formulas, +click the *Formula* tab in the dimension editor. Access the complete +reference for formulas from the help menu. + +The most common formulas are dividing two values to produce a percent. +To display accurately, set *Value format* to *Percent*. + +Filter ratio:: + +Use `kql=''` to filter one set of documents and compare it to other documents within the same grouping. +For example, to see how the error rate changes over time: ++ +``` +count(kql='response.status_code > 400') / count() +``` + +Week over week:: Use `shift='1w'` to get the value of each grouping from +the previous week. Time shift should not be used with the *Top values* function. ++ +``` +percentile(system.network.in.bytes, percentile=99) / +percentile(system.network.in.bytes, percentile=99, shift='1w') +``` + +Percent of total:: Formulas can calculate `overall_sum` for all the groupings, +which lets you convert each grouping into a percent of total: ++ +``` +sum(products.base_price) / overall_sum(sum(products.base_price)) +``` + [float] [[lens-faq]] ==== Frequently asked questions From 6e10c4bfc1ba5783baff81b63fc8fedfe1239d7c Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Wed, 23 Jun 2021 16:06:29 +0200 Subject: [PATCH 107/118] Cypress baseline for osquery (#102265) (#103077) * Cypress baseline for osquery * fix types * Update visual_config.ts Co-authored-by: Patryk Kopycinski Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk Kopycinski Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/typescript/projects.ts | 3 + x-pack/plugins/osquery/cypress/README.md | 138 ++++++++++++++++++ x-pack/plugins/osquery/cypress/cypress.json | 14 ++ .../integration/osquery_manager.spec.ts | 29 ++++ .../plugins/osquery/cypress/plugins/index.js | 29 ++++ .../osquery/cypress/screens/integrations.ts | 10 ++ .../osquery/cypress/screens/navigation.ts | 9 ++ .../osquery/cypress/screens/osquery.ts | 8 + .../osquery/cypress/support/commands.js | 32 ++++ .../plugins/osquery/cypress/support/index.ts | 30 ++++ .../osquery/cypress/tasks/integrations.ts | 20 +++ .../osquery/cypress/tasks/navigation.ts | 19 +++ x-pack/plugins/osquery/cypress/tsconfig.json | 15 ++ x-pack/plugins/osquery/package.json | 13 ++ x-pack/test/osquery_cypress/cli_config.ts | 19 +++ x-pack/test/osquery_cypress/config.ts | 43 ++++++ .../osquery_cypress/ftr_provider_context.d.ts | 12 ++ x-pack/test/osquery_cypress/runner.ts | 81 ++++++++++ x-pack/test/osquery_cypress/services.ts | 8 + x-pack/test/osquery_cypress/visual_config.ts | 19 +++ 20 files changed, 551 insertions(+) create mode 100644 x-pack/plugins/osquery/cypress/README.md create mode 100644 x-pack/plugins/osquery/cypress/cypress.json create mode 100644 x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts create mode 100644 x-pack/plugins/osquery/cypress/plugins/index.js create mode 100644 x-pack/plugins/osquery/cypress/screens/integrations.ts create mode 100644 x-pack/plugins/osquery/cypress/screens/navigation.ts create mode 100644 x-pack/plugins/osquery/cypress/screens/osquery.ts create mode 100644 x-pack/plugins/osquery/cypress/support/commands.js create mode 100644 x-pack/plugins/osquery/cypress/support/index.ts create mode 100644 x-pack/plugins/osquery/cypress/tasks/integrations.ts create mode 100644 x-pack/plugins/osquery/cypress/tasks/navigation.ts create mode 100644 x-pack/plugins/osquery/cypress/tsconfig.json create mode 100644 x-pack/plugins/osquery/package.json create mode 100644 x-pack/test/osquery_cypress/cli_config.ts create mode 100644 x-pack/test/osquery_cypress/config.ts create mode 100644 x-pack/test/osquery_cypress/ftr_provider_context.d.ts create mode 100644 x-pack/test/osquery_cypress/runner.ts create mode 100644 x-pack/test/osquery_cypress/services.ts create mode 100644 x-pack/test/osquery_cypress/visual_config.ts diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 050743114f657..f372cf052d368 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -22,6 +22,9 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), { name: 'security_solution/cypress', }), + new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), { + name: 'osquery/cypress', + }), new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { name: 'apm/cypress', disableTypeCheck: true, diff --git a/x-pack/plugins/osquery/cypress/README.md b/x-pack/plugins/osquery/cypress/README.md new file mode 100644 index 0000000000000..0df311ebc0a05 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/README.md @@ -0,0 +1,138 @@ +# Cypress Tests + +The `osquery/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/). + +## Running the tests + +There are currently three ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below. + +### Execution modes + +#### Interactive mode + +When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview). + +#### Headless mode + +A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser, but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished. + +### Target environments + +#### FTR (CI) + +This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/security_solution_cypress` + +### Test Execution: Examples + +#### FTR + Headless (Chrome) + +Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc. + +```shell +# bootstrap kibana from the project root +yarn kbn bootstrap + +# build the plugins/assets that cypress will execute against +node scripts/build_kibana_platform_plugins + +# launch the cypress test runner +cd x-pack/plugins/security_solution +yarn cypress:run-as-ci +``` +#### FTR + Interactive + +This is the preferred mode for developing new tests. + +```shell +# bootstrap kibana from the project root +yarn kbn bootstrap + +# build the plugins/assets that cypress will execute against +node scripts/build_kibana_platform_plugins + +# launch the cypress test runner +cd x-pack/plugins/security_solution +yarn cypress:open-as-ci +``` + +Note that you can select the browser you want to use on the top right side of the interactive runner. + +## Folder Structure + +### integration/ + +Cypress convention. Contains the specs that are going to be executed. + +### fixtures/ + +Cypress convention. Fixtures are used as external pieces of static data when we stub responses. + +### plugins/ + +Cypress convention. As a convenience, by default Cypress will automatically include the plugins file cypress/plugins/index.js before every single spec file it runs. + +### screens/ + +Contains the elements we want to interact with in our tests. + +Each file inside the screens folder represents a screen in our application. + +### tasks/ + +_Tasks_ are functions that may be reused across tests. + +Each file inside the tasks folder represents a screen of our application. + +## Test data + +The data the tests need: + +- Is generated on the fly using our application APIs (preferred way) +- Is ingested on the ELS instance using the `es_archive` utility + +### How to generate a new archive + +**Note:** As mentioned above, archives are only meant to contain external data, e.g. beats data. Due to the tendency for archived domain objects (rules, signals) to quickly become out of date, it is strongly suggested that you generate this data within the test, through interaction with either the UI or the API. + +We use es_archiver to manage the data that our Cypress tests need. + +1. Set up a clean instance of kibana and elasticsearch (if this is not possible, try to clean/minimize the data that you are going to archive). +2. With the kibana and elasticsearch instance up and running, create the data that you need for your test. +3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution` + +```sh +node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +``` + +Example: + +```sh +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +``` + +Note that the command will create the folder if it does not exist. + +## Development Best Practices + +### Clean up the state + +Remember to clean up the state of the test after its execution, typically with the `cleanKibana` function. Be mindful of failure scenarios, as well: if your test fails, will it leave the environment in a recoverable state? + +### Minimize the use of es_archive + +When possible, create all the data that you need for executing the tests using the application APIS or the UI. + +### Speed up test execution time + +Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be +taken into consideration until another solution is implemented: + +- Group the tests that are similar in different contexts. +- For every context login only once, clean the state between tests if needed without re-loading the page. +- All tests in a spec file must be order-independent. + +Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. + +## Linting + +Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json new file mode 100644 index 0000000000000..eb24616607ec3 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/cypress.json @@ -0,0 +1,14 @@ +{ + "baseUrl": "http://localhost:5620", + "defaultCommandTimeout": 60000, + "execTimeout": 120000, + "pageLoadTimeout": 120000, + "nodeVersion": "system", + "retries": { + "runMode": 2 + }, + "trashAssetsBeforeRuns": false, + "video": false, + "viewportHeight": 900, + "viewportWidth": 1440 +} \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts new file mode 100644 index 0000000000000..0babfd2f10a8e --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.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 { HEADER } from '../screens/osquery'; +import { OSQUERY_NAVIGATION_LINK } from '../screens/navigation'; + +import { INTEGRATIONS, OSQUERY, openNavigationFlyout, navigateTo } from '../tasks/navigation'; +import { addIntegration } from '../tasks/integrations'; + +describe('Osquery Manager', () => { + before(() => { + navigateTo(INTEGRATIONS); + addIntegration('Osquery Manager'); + }); + + it('Displays Osquery on the navigation flyout once installed ', () => { + openNavigationFlyout(); + cy.get(OSQUERY_NAVIGATION_LINK).should('exist'); + }); + + it('Displays Live queries history title when navigating to Osquery', () => { + navigateTo(OSQUERY); + cy.get(HEADER).should('have.text', 'Live queries history'); + }); +}); diff --git a/x-pack/plugins/osquery/cypress/plugins/index.js b/x-pack/plugins/osquery/cypress/plugins/index.js new file mode 100644 index 0000000000000..7dbb69ced7016 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/plugins/index.js @@ -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. + */ + +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +module.exports = (_on, _config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts new file mode 100644 index 0000000000000..0b29e857f46ee --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/integrations.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ADD_POLICY_BTN = '[data-test-subj="addIntegrationPolicyButton"]'; +export const CREATE_PACKAGE_POLICY_SAVE_BTN = '[data-test-subj="createPackagePolicySaveButton"]'; +export const INTEGRATIONS_CARD = '.euiCard__titleAnchor'; diff --git a/x-pack/plugins/osquery/cypress/screens/navigation.ts b/x-pack/plugins/osquery/cypress/screens/navigation.ts new file mode 100644 index 0000000000000..7884cf347d7c0 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/navigation.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]'; +export const OSQUERY_NAVIGATION_LINK = '[data-test-subj="collapsibleNavAppLink"] [title="Osquery"]'; diff --git a/x-pack/plugins/osquery/cypress/screens/osquery.ts b/x-pack/plugins/osquery/cypress/screens/osquery.ts new file mode 100644 index 0000000000000..bc387a57e9e3c --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/osquery.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 HEADER = 'h1'; diff --git a/x-pack/plugins/osquery/cypress/support/commands.js b/x-pack/plugins/osquery/cypress/support/commands.js new file mode 100644 index 0000000000000..66f9435035571 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/commands.js @@ -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. + */ + +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/index.ts new file mode 100644 index 0000000000000..72618c943f4d2 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') +Cypress.on('uncaught:exception', () => { + return false; +}); diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts new file mode 100644 index 0000000000000..f85ef56550af5 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.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 { + ADD_POLICY_BTN, + CREATE_PACKAGE_POLICY_SAVE_BTN, + INTEGRATIONS_CARD, +} from '../screens/integrations'; + +export const addIntegration = (integration: string) => { + cy.get(INTEGRATIONS_CARD).contains(integration).click(); + cy.get(ADD_POLICY_BTN).click(); + cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); + cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).should('not.exist'); + cy.reload(); +}; diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts new file mode 100644 index 0000000000000..63d6b205b433b --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.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 { TOGGLE_NAVIGATION_BTN } from '../screens/navigation'; + +export const INTEGRATIONS = 'app/integrations#/'; +export const OSQUERY = 'app/osquery/live_queries'; + +export const navigateTo = (page: string) => { + cy.visit(page); +}; + +export const openNavigationFlyout = () => { + cy.get(TOGGLE_NAVIGATION_BTN).click(); +}; diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json new file mode 100644 index 0000000000000..467ea13fc4869 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.base.json", + "exclude": [], + "include": [ + "./**/*" + ], + "compilerOptions": { + "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress", + "types": [ + "cypress", + "node" + ], + "resolveJsonModule": true, + }, + } diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json new file mode 100644 index 0000000000000..5bbb95e556d6b --- /dev/null +++ b/x-pack/plugins/osquery/package.json @@ -0,0 +1,13 @@ +{ + "author": "Elastic", + "name": "osquery", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", + "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts", + "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json", + "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts" + } +} diff --git a/x-pack/test/osquery_cypress/cli_config.ts b/x-pack/test/osquery_cypress/cli_config.ts new file mode 100644 index 0000000000000..d0de73151952d --- /dev/null +++ b/x-pack/test/osquery_cypress/cli_config.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 { FtrConfigProviderContext } from '@kbn/test'; + +import { OsqueryCypressCliTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const osqueryCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...osqueryCypressConfig.getAll(), + + testRunner: OsqueryCypressCliTestRunner, + }; +} diff --git a/x-pack/test/osquery_cypress/config.ts b/x-pack/test/osquery_cypress/config.ts new file mode 100644 index 0000000000000..18b4605fb9d8b --- /dev/null +++ b/x-pack/test/osquery_cypress/config.ts @@ -0,0 +1,43 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +import { CA_CERT_PATH } from '@kbn/dev-utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaCommonTestsConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const xpackFunctionalTestsConfig = await readConfigFile( + require.resolve('../functional/config.js') + ); + + return { + ...kibanaCommonTestsConfig.getAll(), + + esTestCluster: { + ...xpackFunctionalTestsConfig.get('esTestCluster'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'), + // define custom es server here + // API Keys is enabled at the top level + 'xpack.security.enabled=true', + ], + }, + + kbnTestServer: { + ...xpackFunctionalTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + // define custom kibana server args here + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ], + }, + }; +} diff --git a/x-pack/test/osquery_cypress/ftr_provider_context.d.ts b/x-pack/test/osquery_cypress/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..aa56557c09df8 --- /dev/null +++ b/x-pack/test/osquery_cypress/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/osquery_cypress/runner.ts b/x-pack/test/osquery_cypress/runner.ts new file mode 100644 index 0000000000000..32c84af5faf76 --- /dev/null +++ b/x-pack/test/osquery_cypress/runner.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. + */ + +import { resolve } from 'path'; +import Url from 'url'; + +import { withProcRunner } from '@kbn/dev-utils'; + +import { FtrProviderContext } from './ftr_provider_context'; + +export async function OsqueryCypressCliTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:run'], + cwd: resolve(__dirname, '../../plugins/osquery'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + ...process.env, + }, + wait: true, + }); + }); +} + +export async function OsqueryCypressVisualTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:open'], + cwd: resolve(__dirname, '../../plugins/osquery'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + ...process.env, + }, + wait: true, + }); + }); +} diff --git a/x-pack/test/osquery_cypress/services.ts b/x-pack/test/osquery_cypress/services.ts new file mode 100644 index 0000000000000..5e063134081ad --- /dev/null +++ b/x-pack/test/osquery_cypress/services.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 * from '../../../test/common/services'; diff --git a/x-pack/test/osquery_cypress/visual_config.ts b/x-pack/test/osquery_cypress/visual_config.ts new file mode 100644 index 0000000000000..35ffe311fdc27 --- /dev/null +++ b/x-pack/test/osquery_cypress/visual_config.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 { FtrConfigProviderContext } from '@kbn/test'; + +import { OsqueryCypressVisualTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const osqueryCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...osqueryCypressConfig.getAll(), + + testRunner: OsqueryCypressVisualTestRunner, + }; +} From 056cf014c0ec74281384df96d5c61b0b82664cd6 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:23:04 -0400 Subject: [PATCH 108/118] Allow restored session to run missing searches and show a warning (#101650) (#103078) * Allow restored session to run missing searches and show a warning * tests and docs * improve warning * tests for new functionality NoSearchIdInSessionError type * managmeent tests * Update texts * fix search service pus * link to docs * imports * format import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Liza Katz --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- ...public.ikibanasearchresponse.isrestored.md | 13 +++ ...ugins-data-public.ikibanasearchresponse.md | 1 + .../kibana-plugin-plugins-data-server.md | 1 + ....nosearchidinsessionerror._constructor_.md | 13 +++ ...ns-data-server.nosearchidinsessionerror.md | 18 ++++ .../public/doc_links/doc_links_service.ts | 2 + src/core/public/public.api.md | 1 + src/plugins/data/common/search/types.ts | 5 + src/plugins/data/public/public.api.md | 1 + .../data/public/search/errors/index.ts | 1 + .../search_session_incomplete_warning.tsx | 31 +++++++ .../search_interceptor.test.ts | 93 +++++++++++++++++++ .../search_interceptor/search_interceptor.ts | 27 ++++++ src/plugins/data/server/index.ts | 1 + .../search/errors/no_search_id_in_session.ts | 15 +++ src/plugins/data/server/search/index.ts | 1 + .../data/server/search/search_service.test.ts | 17 ++++ .../data/server/search/search_service.ts | 43 +++++++-- src/plugins/data/server/server.api.md | 32 ++++--- .../sessions_mgmt/components/status.test.tsx | 1 + .../components/table/table.test.tsx | 6 +- .../search/sessions_mgmt/lib/api.test.ts | 3 + .../public/search/sessions_mgmt/lib/api.ts | 2 + .../sessions_mgmt/lib/get_columns.test.tsx | 36 ++++++- .../search/sessions_mgmt/lib/get_columns.tsx | 14 +++ .../public/search/sessions_mgmt/types.ts | 1 + .../server/search/session/session_service.ts | 8 +- .../api_integration/apis/search/session.ts | 7 +- 30 files changed, 366 insertions(+), 31 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md create mode 100644 src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx create mode 100644 src/plugins/data/server/search/errors/no_search_id_in_session.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index ae433e3db14c6..b10ad949c4944 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -106,6 +106,7 @@ readonly links: { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index b0800c7dfc65e..c020f57faa882 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
    readonly canvas: {
    readonly guide: string;
    };
    readonly dashboard: {
    readonly guide: string;
    readonly drilldowns: string;
    readonly drilldownsTriggerPicker: string;
    readonly urlDrilldownTemplateSyntax: string;
    readonly urlDrilldownVariables: string;
    };
    readonly discover: Record<string, string>;
    readonly filebeat: {
    readonly base: string;
    readonly installation: string;
    readonly configuration: string;
    readonly elasticsearchOutput: string;
    readonly elasticsearchModule: string;
    readonly startup: string;
    readonly exportedFields: string;
    };
    readonly auditbeat: {
    readonly base: string;
    };
    readonly metricbeat: {
    readonly base: string;
    readonly configure: string;
    readonly httpEndpoint: string;
    readonly install: string;
    readonly start: string;
    };
    readonly enterpriseSearch: {
    readonly base: string;
    readonly appSearchBase: string;
    readonly workplaceSearchBase: string;
    };
    readonly heartbeat: {
    readonly base: string;
    };
    readonly logstash: {
    readonly base: string;
    };
    readonly functionbeat: {
    readonly base: string;
    };
    readonly winlogbeat: {
    readonly base: string;
    };
    readonly aggs: {
    readonly composite: string;
    readonly composite_missing_bucket: string;
    readonly date_histogram: string;
    readonly date_range: string;
    readonly date_format_pattern: string;
    readonly filter: string;
    readonly filters: string;
    readonly geohash_grid: string;
    readonly histogram: string;
    readonly ip_range: string;
    readonly range: string;
    readonly significant_terms: string;
    readonly terms: string;
    readonly avg: string;
    readonly avg_bucket: string;
    readonly max_bucket: string;
    readonly min_bucket: string;
    readonly sum_bucket: string;
    readonly cardinality: string;
    readonly count: string;
    readonly cumulative_sum: string;
    readonly derivative: string;
    readonly geo_bounds: string;
    readonly geo_centroid: string;
    readonly max: string;
    readonly median: string;
    readonly min: string;
    readonly moving_avg: string;
    readonly percentile_ranks: string;
    readonly serial_diff: string;
    readonly std_dev: string;
    readonly sum: string;
    readonly top_hits: string;
    };
    readonly runtimeFields: {
    readonly overview: string;
    readonly mapping: string;
    };
    readonly scriptedFields: {
    readonly scriptFields: string;
    readonly scriptAggs: string;
    readonly painless: string;
    readonly painlessApi: string;
    readonly painlessLangSpec: string;
    readonly painlessSyntax: string;
    readonly painlessWalkthrough: string;
    readonly luceneExpressions: string;
    };
    readonly search: {
    readonly sessions: string;
    };
    readonly indexPatterns: {
    readonly introduction: string;
    readonly fieldFormattersNumber: string;
    readonly fieldFormattersString: string;
    readonly runtimeFields: string;
    };
    readonly addData: string;
    readonly kibana: string;
    readonly upgradeAssistant: string;
    readonly rollupJobs: string;
    readonly elasticsearch: Record<string, string>;
    readonly siem: {
    readonly guide: string;
    readonly gettingStarted: string;
    };
    readonly query: {
    readonly eql: string;
    readonly kueryQuerySyntax: string;
    readonly luceneQuerySyntax: string;
    readonly percolate: string;
    readonly queryDsl: string;
    };
    readonly date: {
    readonly dateMath: string;
    readonly dateMathIndexNames: string;
    };
    readonly management: Record<string, string>;
    readonly ml: Record<string, string>;
    readonly transforms: Record<string, string>;
    readonly visualize: Record<string, string>;
    readonly apis: Readonly<{
    bulkIndexAlias: string;
    byteSizeUnits: string;
    createAutoFollowPattern: string;
    createFollower: string;
    createIndex: string;
    createSnapshotLifecyclePolicy: string;
    createRoleMapping: string;
    createRoleMappingTemplates: string;
    createRollupJobsRequest: string;
    createApiKey: string;
    createPipeline: string;
    createTransformRequest: string;
    cronExpressions: string;
    executeWatchActionModes: string;
    indexExists: string;
    openIndex: string;
    putComponentTemplate: string;
    painlessExecute: string;
    painlessExecuteAPIContexts: string;
    putComponentTemplateMetadata: string;
    putSnapshotLifecyclePolicy: string;
    putIndexTemplateV1: string;
    putWatch: string;
    simulatePipeline: string;
    timeUnits: string;
    updateTransform: string;
    }>;
    readonly observability: Record<string, string>;
    readonly alerting: Record<string, string>;
    readonly maps: Record<string, string>;
    readonly monitoring: Record<string, string>;
    readonly security: Readonly<{
    apiKeyServiceSettings: string;
    clusterPrivileges: string;
    elasticsearchSettings: string;
    elasticsearchEnableSecurity: string;
    indicesPrivileges: string;
    kibanaTLS: string;
    kibanaPrivileges: string;
    mappingRoles: string;
    mappingRolesFieldRules: string;
    runAsPrivilege: string;
    }>;
    readonly watcher: Record<string, string>;
    readonly ccs: Record<string, string>;
    readonly plugins: Record<string, string>;
    readonly snapshotRestore: Record<string, string>;
    readonly ingest: Record<string, string>;
    readonly fleet: Readonly<{
    guide: string;
    fleetServer: string;
    fleetServerAddFleetServer: string;
    settings: string;
    settingsFleetServerHostSettings: string;
    troubleshooting: string;
    elasticAgent: string;
    datastreams: string;
    datastreamsNamingScheme: string;
    upgradeElasticAgent: string;
    upgradeElasticAgent712lower: string;
    }>;
    } | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
    readonly canvas: {
    readonly guide: string;
    };
    readonly dashboard: {
    readonly guide: string;
    readonly drilldowns: string;
    readonly drilldownsTriggerPicker: string;
    readonly urlDrilldownTemplateSyntax: string;
    readonly urlDrilldownVariables: string;
    };
    readonly discover: Record<string, string>;
    readonly filebeat: {
    readonly base: string;
    readonly installation: string;
    readonly configuration: string;
    readonly elasticsearchOutput: string;
    readonly elasticsearchModule: string;
    readonly startup: string;
    readonly exportedFields: string;
    };
    readonly auditbeat: {
    readonly base: string;
    };
    readonly metricbeat: {
    readonly base: string;
    readonly configure: string;
    readonly httpEndpoint: string;
    readonly install: string;
    readonly start: string;
    };
    readonly enterpriseSearch: {
    readonly base: string;
    readonly appSearchBase: string;
    readonly workplaceSearchBase: string;
    };
    readonly heartbeat: {
    readonly base: string;
    };
    readonly logstash: {
    readonly base: string;
    };
    readonly functionbeat: {
    readonly base: string;
    };
    readonly winlogbeat: {
    readonly base: string;
    };
    readonly aggs: {
    readonly composite: string;
    readonly composite_missing_bucket: string;
    readonly date_histogram: string;
    readonly date_range: string;
    readonly date_format_pattern: string;
    readonly filter: string;
    readonly filters: string;
    readonly geohash_grid: string;
    readonly histogram: string;
    readonly ip_range: string;
    readonly range: string;
    readonly significant_terms: string;
    readonly terms: string;
    readonly avg: string;
    readonly avg_bucket: string;
    readonly max_bucket: string;
    readonly min_bucket: string;
    readonly sum_bucket: string;
    readonly cardinality: string;
    readonly count: string;
    readonly cumulative_sum: string;
    readonly derivative: string;
    readonly geo_bounds: string;
    readonly geo_centroid: string;
    readonly max: string;
    readonly median: string;
    readonly min: string;
    readonly moving_avg: string;
    readonly percentile_ranks: string;
    readonly serial_diff: string;
    readonly std_dev: string;
    readonly sum: string;
    readonly top_hits: string;
    };
    readonly runtimeFields: {
    readonly overview: string;
    readonly mapping: string;
    };
    readonly scriptedFields: {
    readonly scriptFields: string;
    readonly scriptAggs: string;
    readonly painless: string;
    readonly painlessApi: string;
    readonly painlessLangSpec: string;
    readonly painlessSyntax: string;
    readonly painlessWalkthrough: string;
    readonly luceneExpressions: string;
    };
    readonly search: {
    readonly sessions: string;
    readonly sessionLimits: string;
    };
    readonly indexPatterns: {
    readonly introduction: string;
    readonly fieldFormattersNumber: string;
    readonly fieldFormattersString: string;
    readonly runtimeFields: string;
    };
    readonly addData: string;
    readonly kibana: string;
    readonly upgradeAssistant: string;
    readonly elasticsearch: Record<string, string>;
    readonly siem: {
    readonly guide: string;
    readonly gettingStarted: string;
    };
    readonly query: {
    readonly eql: string;
    readonly kueryQuerySyntax: string;
    readonly luceneQuerySyntax: string;
    readonly percolate: string;
    readonly queryDsl: string;
    };
    readonly date: {
    readonly dateMath: string;
    readonly dateMathIndexNames: string;
    };
    readonly management: Record<string, string>;
    readonly ml: Record<string, string>;
    readonly transforms: Record<string, string>;
    readonly visualize: Record<string, string>;
    readonly apis: Readonly<{
    bulkIndexAlias: string;
    byteSizeUnits: string;
    createAutoFollowPattern: string;
    createFollower: string;
    createIndex: string;
    createSnapshotLifecyclePolicy: string;
    createRoleMapping: string;
    createRoleMappingTemplates: string;
    createRollupJobsRequest: string;
    createApiKey: string;
    createPipeline: string;
    createTransformRequest: string;
    cronExpressions: string;
    executeWatchActionModes: string;
    indexExists: string;
    openIndex: string;
    putComponentTemplate: string;
    painlessExecute: string;
    painlessExecuteAPIContexts: string;
    putComponentTemplateMetadata: string;
    putSnapshotLifecyclePolicy: string;
    putIndexTemplateV1: string;
    putWatch: string;
    simulatePipeline: string;
    timeUnits: string;
    updateTransform: string;
    }>;
    readonly observability: Record<string, string>;
    readonly alerting: Record<string, string>;
    readonly maps: Record<string, string>;
    readonly monitoring: Record<string, string>;
    readonly security: Readonly<{
    apiKeyServiceSettings: string;
    clusterPrivileges: string;
    elasticsearchSettings: string;
    elasticsearchEnableSecurity: string;
    indicesPrivileges: string;
    kibanaTLS: string;
    kibanaPrivileges: string;
    mappingRoles: string;
    mappingRolesFieldRules: string;
    runAsPrivilege: string;
    }>;
    readonly watcher: Record<string, string>;
    readonly ccs: Record<string, string>;
    readonly plugins: Record<string, string>;
    readonly snapshotRestore: Record<string, string>;
    readonly ingest: Record<string, string>;
    } | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md new file mode 100644 index 0000000000000..d649212ae0547 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) > [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) + +## IKibanaSearchResponse.isRestored property + +Indicates whether the results returned are from the async-search index + +Signature: + +```typescript +isRestored?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md index 1d3e0c08dfc18..c7046902dac72 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md @@ -16,6 +16,7 @@ export interface IKibanaSearchResponse | --- | --- | --- | | [id](./kibana-plugin-plugins-data-public.ikibanasearchresponse.id.md) | string | Some responses may contain a unique id to identify the request this response came from. | | [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | boolean | Indicates whether the results returned are complete or partial | +| [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) | boolean | Indicates whether the results returned are from the async-search index | | [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | boolean | Indicates whether search is still in flight | | [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | number | If relevant to the search strategy, return a loaded number that represents how progress is indicated. | | [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | RawResponse | The raw response returned by the internal search method (usually the raw ES response) | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index b1745b298e27e..9816b884c4614 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -13,6 +13,7 @@ | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | | +| [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md new file mode 100644 index 0000000000000..e48a1c98f8578 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) > [(constructor)](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) + +## NoSearchIdInSessionError.(constructor) + +Constructs a new instance of the `NoSearchIdInSessionError` class + +Signature: + +```typescript +constructor(); +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md new file mode 100644 index 0000000000000..707739f845cd1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) + +## NoSearchIdInSessionError class + +Signature: + +```typescript +export declare class NoSearchIdInSessionError extends KbnError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)()](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) | | Constructs a new instance of the NoSearchIdInSessionError class | + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index c737f41cdabc5..f19ae2d9af3c2 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -205,6 +205,7 @@ export class DocLinksService { }, search: { sessions: `${KIBANA_DOCS}search-sessions.html`, + sessionLimits: `${KIBANA_DOCS}search-sessions.html#_limitations`, }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, @@ -525,6 +526,7 @@ export interface DocLinksStart { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 27569935bcc65..31e85341fb519 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -585,6 +585,7 @@ export interface DocLinksStart { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index d1890ec97df4e..c5cf3f9f09e6c 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -65,6 +65,11 @@ export interface IKibanaSearchResponse { */ isPartial?: boolean; + /** + * Indicates whether the results returned are from the async-search index + */ + isRestored?: boolean; + /** * The raw response returned by the internal search method (usually the raw ES response) */ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 8e2f28d75166a..9076b0a5770f1 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1353,6 +1353,7 @@ export interface IKibanaSearchRequest { export interface IKibanaSearchResponse { id?: string; isPartial?: boolean; + isRestored?: boolean; isRunning?: boolean; loaded?: number; rawResponse: RawResponse; diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts index 82c9e04b79798..fcdea8dec1c2e 100644 --- a/src/plugins/data/public/search/errors/index.ts +++ b/src/plugins/data/public/search/errors/index.ts @@ -12,3 +12,4 @@ export * from './timeout_error'; export * from './utils'; export * from './types'; export * from './http_error'; +export * from './search_session_incomplete_warning'; diff --git a/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx new file mode 100644 index 0000000000000..c5c5c37f31cf8 --- /dev/null +++ b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const SearchSessionIncompleteWarning = (docLinks: CoreStart['docLinks']) => ( + <> + + It needs more time to fully render. You can wait here or come back to it later. + + + + + + + +); diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index fe66d4b6e9937..155638250a2a4 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -29,6 +29,12 @@ jest.mock('./utils', () => ({ }), })); +jest.mock('../errors/search_session_incomplete_warning', () => ({ + SearchSessionIncompleteWarning: jest.fn(), +})); + +import { SearchSessionIncompleteWarning } from '../errors/search_session_incomplete_warning'; + let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; let bfetchSetup: jest.Mocked; @@ -508,6 +514,7 @@ describe('SearchInterceptor', () => { } : null ); + sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore); fetchMock.mockResolvedValue({ result: 200 }); }; @@ -562,6 +569,92 @@ describe('SearchInterceptor', () => { (sessionService as jest.Mocked).getSearchOptions ).toHaveBeenCalledWith(sessionId); }); + + test('should not show warning if a search is available during restore', async () => { + setup({ + isRestore: true, + isStored: true, + sessionId: '123', + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: true, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + const response = searchInterceptor.search( + {}, + { + sessionId: '123', + } + ); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); + }); + + test('should show warning once if a search is not available during restore', async () => { + setup({ + isRestore: true, + isStored: true, + sessionId: '123', + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: false, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + searchInterceptor + .search( + {}, + { + sessionId: '123', + } + ) + .subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(1); + + searchInterceptor + .search( + {}, + { + sessionId: '123', + } + ) + .subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(1); + }); }); describe('Session tracking', () => { diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 57b156a9b3c00..e0e1df65101c7 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -43,6 +43,7 @@ import { PainlessError, SearchTimeoutError, TimeoutErrorMode, + SearchSessionIncompleteWarning, } from '../errors'; import { toMountPoint } from '../../../../kibana_react/public'; import { AbortError, KibanaServerError } from '../../../../kibana_utils/public'; @@ -82,6 +83,7 @@ export class SearchInterceptor { * @internal */ private application!: CoreStart['application']; + private docLinks!: CoreStart['docLinks']; private batchedFetch!: BatchedFunc< { request: IKibanaSearchRequest; options: ISearchOptionsSerializable }, IKibanaSearchResponse @@ -95,6 +97,7 @@ export class SearchInterceptor { this.deps.startServices.then(([coreStart]) => { this.application = coreStart.application; + this.docLinks = coreStart.docLinks; }); this.batchedFetch = deps.bfetch.batchedFunction({ @@ -345,6 +348,11 @@ export class SearchInterceptor { this.handleSearchError(e, searchOptions, searchAbortController.isTimeout()) ); }), + tap((response) => { + if (this.deps.session.isRestore() && response.isRestored === false) { + this.showRestoreWarning(this.deps.session.getSessionId()); + } + }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); if (untrackSearch && this.deps.session.isCurrentSession(sessionId)) { @@ -371,6 +379,25 @@ export class SearchInterceptor { } ); + private showRestoreWarningToast = (sessionId?: string) => { + this.deps.toasts.addWarning( + { + title: 'Your search session is still running', + text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)), + }, + { + toastLifeTimeMs: 60000, + } + ); + }; + + private showRestoreWarning = memoize( + this.showRestoreWarningToast, + (_: SearchTimeoutError, sessionId: string) => { + return sessionId; + } + ); + /** * Show one error notification per session. * @internal diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 0764f4f441e42..dd60951e6d228 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -238,6 +238,7 @@ export { DataRequestHandlerContext, AsyncSearchResponse, AsyncSearchStatusResponse, + NoSearchIdInSessionError, } from './search'; // Search namespace diff --git a/src/plugins/data/server/search/errors/no_search_id_in_session.ts b/src/plugins/data/server/search/errors/no_search_id_in_session.ts new file mode 100644 index 0000000000000..b291df1cee5ba --- /dev/null +++ b/src/plugins/data/server/search/errors/no_search_id_in_session.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KbnError } from '../../../../kibana_utils/common'; + +export class NoSearchIdInSessionError extends KbnError { + constructor() { + super('No search ID in this session matching the given search request'); + } +} diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 812f3171aef99..b9affe96ea2dd 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -13,3 +13,4 @@ export * from './strategies/eql_search'; export { usageProvider, SearchUsage, searchUsageObserver } from './collectors'; export * from './aggs'; export * from './session'; +export * from './errors/no_search_id_in_session'; diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 52ee8e60a5b26..314cb2c3acbf8 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -25,6 +25,7 @@ import { ISearchSessionService, ISearchStart, ISearchStrategy, + NoSearchIdInSessionError, } from '.'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { expressionsPluginMock } from '../../../expressions/public/mocks'; @@ -175,6 +176,22 @@ describe('Search service', () => { expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' }); }); + it('searches even if id is not found in session during restore', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + mockSessionClient.getId = jest.fn().mockImplementation(() => { + throw new NoSearchIdInSessionError(); + }); + + const res = await mockScopedClient.search(searchRequest, options).toPromise(); + + const [request, callOptions] = mockStrategy.search.mock.calls[0]; + expect(callOptions).toBe(options); + expect(request).toStrictEqual({ ...searchRequest }); + expect(res.isRestored).toBe(false); + }); + it('does not fail if `trackId` throws', async () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: false, isRestore: false }; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a651d7b3bf105..00dffefa5e3a6 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -19,7 +19,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { first, switchMap, tap } from 'rxjs/operators'; +import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import type { @@ -80,6 +80,7 @@ import { registerBsearchRoute } from './routes/bsearch'; import { getKibanaContext } from './expressions/kibana_context'; import { enhancedEsSearchStrategyProvider } from './strategies/ese_search'; import { eqlSearchStrategyProvider } from './strategies/eql_search'; +import { NoSearchIdInSessionError } from './errors/no_search_id_in_session'; type StrategyMap = Record>; @@ -287,24 +288,48 @@ export class SearchService implements Plugin { options.strategy ); - const getSearchRequest = async () => - !options.sessionId || !options.isRestore || request.id - ? request - : { + const getSearchRequest = async () => { + if (!options.sessionId || !options.isRestore || request.id) { + return request; + } else { + try { + const id = await deps.searchSessionsClient.getId(request, options); + this.logger.debug(`Found search session id for request ${id}`); + return { ...request, - id: await deps.searchSessionsClient.getId(request, options), + id, }; + } catch (e) { + if (e instanceof NoSearchIdInSessionError) { + this.logger.debug('Ignoring missing search ID'); + return request; + } else { + throw e; + } + } + } + }; - return from(getSearchRequest()).pipe( + const searchRequest$ = from(getSearchRequest()); + const search$ = searchRequest$.pipe( switchMap((searchRequest) => strategy.search(searchRequest, options, deps)), - tap((response) => { - if (!options.sessionId || !response.id || options.isRestore) return; + withLatestFrom(searchRequest$), + tap(([response, requestWithId]) => { + if (!options.sessionId || !response.id || (options.isRestore && requestWithId.id)) return; // intentionally swallow tracking error, as it shouldn't fail the search deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => { this.logger.error(trackErr); }); + }), + map(([response, requestWithId]) => { + return { + ...response, + isRestored: !!requestWithId.id, + }; }) ); + + return search$; } catch (e) { return throwError(e); } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f0466dd317ab7..44be1a2255433 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1211,6 +1211,14 @@ export enum METRIC_TYPES { TOP_HITS = "top_hits" } +// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "NoSearchIdInSessionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class NoSearchIdInSessionError extends KbnError { + constructor(); +} + // Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1543,18 +1551,18 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx index 86f5564a17d52..59da0f0f4d17e 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx @@ -27,6 +27,7 @@ describe('Background Search Session management status labels', () => { id: 'wtywp9u2802hahgp-gsla', restoreUrl: '/app/great-app-url/#45', reloadUrl: '/app/great-app-url/#45', + numSearches: 1, appId: 'security', status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 42ff270ed44a0..6dfe3a5153670 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -70,6 +70,7 @@ describe('Background Search Session Management Table', () => { status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', expires: '2020-12-07T00:19:32Z', + idMapping: {}, }, }, ], @@ -95,10 +96,12 @@ describe('Background Search Session Management Table', () => { ); }); - expect(table.find('thead th').map((node) => node.text())).toMatchInlineSnapshot(` + expect(table.find('thead th .euiTableCellContent__text').map((node) => node.text())) + .toMatchInlineSnapshot(` Array [ "App", "Name", + "# Searches", "Status", "Created", "Expiration", @@ -130,6 +133,7 @@ describe('Background Search Session Management Table', () => { Array [ "App", "Namevery background search ", + "# Searches0", "StatusExpired", "Created2 Dec, 2020, 00:19:32", "Expiration--", 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 3857b08ad0a3a..cc79f8002a98c 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 @@ -52,6 +52,7 @@ describe('Search Sessions Management API', () => { status: 'complete', initialState: {}, restoreState: {}, + idMapping: [], }, }, ], @@ -78,6 +79,7 @@ describe('Search Sessions Management API', () => { "id": "hello-pizza-123", "initialState": Object {}, "name": "Veggie", + "numSearches": 0, "reloadUrl": "hello-cool-undefined-url", "restoreState": Object {}, "restoreUrl": "hello-cool-undefined-url", @@ -100,6 +102,7 @@ describe('Search Sessions Management API', () => { expires: moment().subtract(3, 'days'), initialState: {}, restoreState: {}, + idMapping: {}, }, }, ], 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 3710dfa16e76b..0369dc4a839b5 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 @@ -90,6 +90,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) urlGeneratorId, initialState, restoreState, + idMapping, } = savedObject.attributes; const status = getUIStatus(savedObject.attributes); @@ -113,6 +114,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) reloadUrl, initialState, restoreState, + numSearches: Object.keys(idMapping).length, }; }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx index 4b68e0c9e2afd..fc4e67360ea4a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx @@ -70,6 +70,7 @@ describe('Search Sessions Management table column factory', () => { reloadUrl: '/app/great-app-url', restoreUrl: '/app/great-app-url/#42', appId: 'discovery', + numSearches: 3, status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', expires: '2020-12-07T00:19:32Z', @@ -95,6 +96,12 @@ describe('Search Sessions Management table column factory', () => { "sortable": true, "width": "20%", }, + Object { + "field": "numSearches", + "name": "# Searches", + "render": [Function], + "sortable": true, + }, Object { "field": "status", "name": "Status", @@ -146,10 +153,29 @@ describe('Search Sessions Management table column factory', () => { }); }); + // Num of searches column + describe('num of searches', () => { + test('renders', () => { + const [, , numOfSearches] = getColumns( + mockCoreStart, + mockPluginsSetup, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + const numOfSearchesLine = mount( + numOfSearches.render!(mockSession.numSearches, mockSession) as ReactElement + ); + expect(numOfSearchesLine.text()).toMatchInlineSnapshot(`"3"`); + }); + }); + // Status column describe('status', () => { test('render in_progress', () => { - const [, , status] = getColumns( + const [, , , status] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -165,7 +191,7 @@ describe('Search Sessions Management table column factory', () => { }); test('error handling', () => { - const [, , status] = getColumns( + const [, , , status] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -188,7 +214,7 @@ describe('Search Sessions Management table column factory', () => { test('render using Browser timezone', () => { tz = 'Browser'; - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -205,7 +231,7 @@ describe('Search Sessions Management table column factory', () => { test('render using AK timezone', () => { tz = 'US/Alaska'; - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -220,7 +246,7 @@ describe('Search Sessions Management table column factory', () => { }); test('error handling', () => { - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx index 1805ef52b85f1..d8d2fa0aeac59 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx @@ -120,6 +120,20 @@ export const getColumns = ( }, }, + // # Searches + { + field: 'numSearches', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.numSearches', { + defaultMessage: '# Searches', + }), + sortable: true, + render: (numSearches: UISession['numSearches'], session) => ( + + {numSearches} + + ), + }, + // Session status { field: 'status', diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts index d0d5ee9fb17dd..6a8ace8dbdc79 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts @@ -34,6 +34,7 @@ export interface UISession { created: string; expires: string | null; status: UISearchSessionState; + numSearches: number; actions?: ACTION[]; reloadUrl: string; restoreUrl: string; diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 138f42549a094..81a12f607935d 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -24,7 +24,11 @@ import { ENHANCED_ES_SEARCH_STRATEGY, SEARCH_SESSION_TYPE, } from '../../../../../../src/plugins/data/common'; -import { esKuery, ISearchSessionService } from '../../../../../../src/plugins/data/server'; +import { + esKuery, + ISearchSessionService, + NoSearchIdInSessionError, +} from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server'; import { TaskManagerSetupContract, @@ -436,7 +440,7 @@ export class SearchSessionService const requestHash = createRequestHash(searchRequest.params); if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { this.logger.error(`getId | ${sessionId} | ${requestHash} not found`); - throw new Error('No search ID in this session matching the given search request'); + throw new NoSearchIdInSessionError(); } this.logger.debug(`getId | ${sessionId} | ${requestHash}`); diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index d47199a0f1c1e..06be7c6759bc0 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -403,7 +403,12 @@ export default function ({ getService }: FtrProviderContext) { const { id: id1 } = searchRes1.body; // it might take the session a moment to be created - await new Promise((resolve) => setTimeout(resolve, 2500)); + await retry.waitFor('search session created', async () => { + const response = await supertest + .get(`/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo'); + return response.body.statusCode === undefined; + }); const getSessionFirstTime = await supertest .get(`/internal/session/${sessionId}`) From 0f699fd013b984f7d567425cade00c6f511f499f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:29:15 -0400 Subject: [PATCH 109/118] [Fleet] Improve default port experience in the settings UI (#102982) (#103079) Co-authored-by: Nicolas Chaulet --- .../services/hosts_utils.test.ts | 0 .../services/hosts_utils.ts | 0 x-pack/plugins/fleet/common/services/index.ts | 2 + .../components/settings_flyout/index.tsx | 47 ++++++++++++++----- .../plugins/fleet/server/services/output.ts | 3 +- .../plugins/fleet/server/services/settings.ts | 7 ++- 6 files changed, 42 insertions(+), 17 deletions(-) rename x-pack/plugins/fleet/{server => common}/services/hosts_utils.test.ts (100%) rename x-pack/plugins/fleet/{server => common}/services/hosts_utils.ts (100%) diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.test.ts b/x-pack/plugins/fleet/common/services/hosts_utils.test.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/hosts_utils.test.ts rename to x-pack/plugins/fleet/common/services/hosts_utils.test.ts diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.ts b/x-pack/plugins/fleet/common/services/hosts_utils.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/hosts_utils.ts rename to x-pack/plugins/fleet/common/services/hosts_utils.ts diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 86361ae163399..a6f4cd319b970 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -30,3 +30,5 @@ export { validationHasErrors, countValidationErrors, } from './validate_package_policy'; + +export { normalizeHostsForAgents } from './hosts_utils'; diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx index d748e655bd506..9bc1bc977b786 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx @@ -38,7 +38,7 @@ import { useGetOutputs, sendPutOutput, } from '../../hooks'; -import { isDiffPathProtocol } from '../../../common'; +import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common'; import { SettingsConfirmModal } from './confirm_modal'; import type { SettingsConfirmModalProps } from './confirm_modal'; @@ -53,8 +53,20 @@ interface Props { onClose: () => void; } -function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) { - return arrayA.length === arrayB.length && arrayA.every((val, index) => val === arrayB[index]); +function normalizeHosts(hostsInput: string[]) { + return hostsInput.map((host) => { + try { + return normalizeHostsForAgents(host); + } catch (err) { + return host; + } + }); +} + +function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) { + const hostsA = normalizeHosts(arrayA); + const hostsB = normalizeHosts(arrayB); + return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]); } function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { @@ -234,8 +246,11 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { return false; } return ( - !isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value) || - !isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value) || + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) || + !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) || (output.config_yaml || '') !== inputs.additionalYamlConfig.value ); }, [settings, inputs, output]); @@ -246,32 +261,37 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { } const tmpChanges: SettingsConfirmModalProps['changes'] = []; - if (!isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value)) { + if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) { tmpChanges.push( { type: 'elasticsearch', direction: 'removed', - urls: output.hosts || [], + urls: normalizeHosts(output.hosts || []), }, { type: 'elasticsearch', direction: 'added', - urls: inputs.elasticsearchUrl.value, + urls: normalizeHosts(inputs.elasticsearchUrl.value), } ); } - if (!isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value)) { + if ( + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) + ) { tmpChanges.push( { type: 'fleet_server', direction: 'removed', - urls: settings.fleet_server_hosts, + urls: normalizeHosts(settings.fleet_server_hosts || []), }, { type: 'fleet_server', direction: 'added', - urls: inputs.fleetServerHosts.value, + urls: normalizeHosts(inputs.fleetServerHosts.value), } ); } @@ -300,7 +320,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { helpText={ = ({ onClose }) => { defaultMessage: 'Elasticsearch hosts', })} helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', { - defaultMessage: 'Specify the Elasticsearch URLs where agents send data.', + defaultMessage: + 'Specify the Elasticsearch URLs where agents send data. Elasticsearch uses port 9200 by default.', })} />
    diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 0c7b086f78fdf..8c6bc7eca0401 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -9,10 +9,9 @@ import type { SavedObjectsClientContract } from 'src/core/server'; import type { NewOutput, Output, OutputSOAttributes } from '../types'; import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; -import { decodeCloudId } from '../../common'; +import { decodeCloudId, normalizeHostsForAgents } from '../../common'; import { appContextService } from './app_context'; -import { normalizeHostsForAgents } from './hosts_utils'; const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE; diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 226fbb29467c2..26d581f32d9a2 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -8,11 +8,14 @@ import Boom from '@hapi/boom'; import type { SavedObjectsClientContract } from 'kibana/server'; -import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common'; +import { + decodeCloudId, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + normalizeHostsForAgents, +} from '../../common'; import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common'; import { appContextService } from './app_context'; -import { normalizeHostsForAgents } from './hosts_utils'; export async function getSettings(soClient: SavedObjectsClientContract): Promise { const res = await soClient.find({ From f073f0efc35a5d6d6d932c4d8de8c4c2f0e55403 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 11:56:23 -0400 Subject: [PATCH 110/118] Adjust copy for non-removable integrations/packages (#103068) (#103092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandro Fernández Gómez --- .../sections/epm/screens/detail/settings/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 995423ea91f96..9e8d200344b01 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -233,7 +233,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { , From a0c15f4dfaca797f2eb529fdeda60c6ff2fe5488 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:09:50 -0400 Subject: [PATCH 111/118] [Security Solution][Endpoint] Hide endpoint event filters list in detections tab (#102644) (#103097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add event filters filter on exception list to hide it in UI * Fixes unit test and added more tests for showEventFilters * fixes test adding showEventFilters test cases * Pass params as js object instead of individual variables Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: David Sánchez --- .../src/typescript_types/index.ts | 1 + .../src/use_exception_lists/index.ts | 7 +- .../get_event_filters_filter/index.test.ts | 39 +++ .../src/get_event_filters_filter/index.ts | 27 ++ .../src/get_filters/index.test.ts | 274 ++++++++++++++++-- .../src/get_filters/index.ts | 24 +- .../hooks/use_exception_lists.test.ts | 89 +++++- .../rules/all/exceptions/exceptions_table.tsx | 1 + 8 files changed, 420 insertions(+), 42 deletions(-) create mode 100644 packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index f75f0dcebf4f6..1909bcb1bcc2e 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -42,6 +42,7 @@ export interface UseExceptionListsProps { notifications: NotificationsStart; pagination?: Pagination; showTrustedApps: boolean; + showEventFilters: boolean; } export interface UseExceptionListProps { diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts index a9a93aa8df49a..0bd4c6c705668 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts @@ -28,6 +28,7 @@ export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, * @param namespaceTypes spaces to be searched * @param notifications kibana service for displaying toasters * @param showTrustedApps boolean - include/exclude trusted app lists + * @param showEventFilters boolean - include/exclude event filters lists * @param pagination * */ @@ -43,6 +44,7 @@ export const useExceptionLists = ({ namespaceTypes, notifications, showTrustedApps = false, + showEventFilters = false, }: UseExceptionListsProps): ReturnExceptionLists => { const [exceptionLists, setExceptionLists] = useState([]); const [paginationInfo, setPagination] = useState(pagination); @@ -51,8 +53,9 @@ export const useExceptionLists = ({ const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]); const filters = useMemo( - (): string => getFilters(filterOptions, namespaceTypes, showTrustedApps), - [namespaceTypes, filterOptions, showTrustedApps] + (): string => + getFilters({ filters: filterOptions, namespaceTypes, showTrustedApps, showEventFilters }), + [namespaceTypes, filterOptions, showTrustedApps, showEventFilters] ); useEffect(() => { diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts new file mode 100644 index 0000000000000..934a9cbff56a6 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getEventFiltersFilter } from '.'; + +describe('getEventFiltersFilter', () => { + test('it returns filter to search for "exception-list" namespace trusted apps', () => { + const filter = getEventFiltersFilter(true, ['exception-list']); + + expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_event_filters*)'); + }); + + test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getEventFiltersFilter(true, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it returns filter to exclude "exception-list" namespace trusted apps', () => { + const filter = getEventFiltersFilter(false, ['exception-list']); + + expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_event_filters*)'); + }); + + test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getEventFiltersFilter(false, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts new file mode 100644 index 0000000000000..7e55073228fca --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { SavedObjectType } from '../types'; + +export const getEventFiltersFilter = ( + showEventFilter: boolean, + namespaceTypes: SavedObjectType[] +): string => { + if (showEventFilter) { + const filters = namespaceTypes.map((namespace) => { + return `${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`; + }); + return `(${filters.join(' OR ')})`; + } else { + const filters = namespaceTypes.map((namespace) => { + return `not ${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`; + }); + return `(${filters.join(' AND ')})`; + } +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts index 327a29dc1b987..bfaad52ee8147 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts @@ -11,106 +11,318 @@ import { getFilters } from '.'; describe('getFilters', () => { describe('single', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); - expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: true, + showEventFilters: false, + }); - expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' ); }); test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it if filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' ); }); }); describe('agnostic', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['agnostic'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['agnostic'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it if filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); }); describe('single, agnostic', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single', 'agnostic'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single', 'agnostic'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - false - ); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is true', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - true + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); + }); + + test('it properly formats when filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); }); diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts index c9dd6ccae484c..238ae5541343c 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts @@ -10,14 +10,26 @@ import { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts- import { getGeneralFilters } from '../get_general_filters'; import { getSavedObjectTypes } from '../get_saved_object_types'; import { getTrustedAppsFilter } from '../get_trusted_apps_filter'; +import { getEventFiltersFilter } from '../get_event_filters_filter'; -export const getFilters = ( - filters: ExceptionListFilter, - namespaceTypes: NamespaceType[], - showTrustedApps: boolean -): string => { +export interface GetFiltersParams { + filters: ExceptionListFilter; + namespaceTypes: NamespaceType[]; + showTrustedApps: boolean; + showEventFilters: boolean; +} + +export const getFilters = ({ + filters, + namespaceTypes, + showTrustedApps, + showEventFilters, +}: GetFiltersParams): string => { const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes }); const generalFilters = getGeneralFilters(filters, namespaces); const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces); - return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND '); + const eventFiltersFilter = getEventFiltersFilter(showEventFilters, namespaces); + return [generalFilters, trustedAppsFilter, eventFiltersFilter] + .filter((filter) => filter.trim() !== '') + .join(' AND '); }; diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts index bdcb4224eed9c..4987de321c556 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts @@ -48,6 +48,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -83,6 +84,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -122,6 +124,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: true, }) ); @@ -132,7 +135,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -157,6 +160,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -167,7 +171,79 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches event filters lists if "showEventFilters" is true', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showEventFilters: true, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('does not fetch event filters lists if "showEventFilters" is false', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showEventFilters: false, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -195,6 +271,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -205,7 +282,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -228,6 +305,7 @@ describe('useExceptionLists', () => { namespaceTypes, notifications, pagination, + showEventFilters, showTrustedApps, }) => useExceptionLists({ @@ -237,6 +315,7 @@ describe('useExceptionLists', () => { namespaceTypes, notifications, pagination, + showEventFilters, showTrustedApps, }), { @@ -251,6 +330,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }, } @@ -271,6 +351,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }); // NOTE: Only need one call here because hook already initilaized @@ -298,6 +379,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -336,6 +418,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 7f734b10fd020..19a3b0b38e53a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -78,6 +78,7 @@ export const ExceptionListsTable = React.memo( namespaceTypes: ['single', 'agnostic'], notifications, showTrustedApps: false, + showEventFilters: false, }); const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists( { From 6a8007bb694537be42ad36bf18a9fc8659358b47 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:25:30 -0400 Subject: [PATCH 112/118] Adds a versioned class name to a root DOM element (#102443) (#103101) Co-authored-by: Christiane (Tina) Heiligers --- src/core/public/chrome/chrome_service.test.ts | 54 ++++++++++++++++++- src/core/public/chrome/chrome_service.tsx | 13 +++++ src/core/public/core_system.test.ts | 7 +-- src/core/public/core_system.ts | 8 +-- src/core/public/public.api.md | 2 +- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 0264c8a1acf75..92f5a854f6b00 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -53,8 +53,21 @@ function defaultStartDeps(availableApps?: App[]) { return deps; } +function defaultStartTestOptions({ + browserSupportsCsp = true, + kibanaVersion = 'version', +}: { + browserSupportsCsp?: boolean; + kibanaVersion?: string; +}): any { + return { + browserSupportsCsp, + kibanaVersion, + }; +} + async function start({ - options = { browserSupportsCsp: true }, + options = defaultStartTestOptions({}), cspConfigMock = { warnLegacyBrowsers: true }, startDeps = defaultStartDeps(), }: { options?: any; cspConfigMock?: any; startDeps?: ReturnType } = {}) { @@ -82,7 +95,9 @@ afterAll(() => { describe('start', () => { it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => { - const { startDeps } = await start({ options: { browserSupportsCsp: false } }); + const { startDeps } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '7.0.0' }, + }); expect(startDeps.notifications.toasts.addWarning.mock.calls).toMatchInlineSnapshot(` Array [ @@ -95,6 +110,41 @@ describe('start', () => { `); }); + it('adds the kibana versioned class to the document body', async () => { + const { chrome, service } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '1.2.3' }, + }); + const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise(); + service.stop(); + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + "kbnBody", + "kbnBody--noHeaderBanner", + "kbnBody--chromeHidden", + "kbnVersion-1-2-3", + ], + ] + `); + }); + it('strips off "snapshot" from the kibana version if present', async () => { + const { chrome, service } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '8.0.0-SnAPshot' }, + }); + const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise(); + service.stop(); + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + "kbnBody", + "kbnBody--noHeaderBanner", + "kbnBody--chromeHidden", + "kbnVersion-8-0-0", + ], + ] + `); + }); + it('does not add legacy browser warning if browser supports CSP', async () => { const { startDeps } = await start(); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 5ed447edde75a..f1381c52ce779 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -37,9 +37,11 @@ import { export type { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; +const SNAPSHOT_REGEX = /-snapshot/i; interface ConstructorParams { browserSupportsCsp: boolean; + kibanaVersion: string; } interface StartDeps { @@ -116,6 +118,16 @@ export class ChromeService { const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); + const getKbnVersionClass = () => { + // we assume that the version is valid and has the form 'X.X.X' + // strip out `SNAPSHOT` and reformat to 'X-X-X' + const formattedVersionClass = this.params.kibanaVersion + .replace(SNAPSHOT_REGEX, '') + .split('.') + .join('-'); + return `kbnVersion-${formattedVersionClass}`; + }; + const headerBanner$ = new BehaviorSubject(undefined); const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe( map(([headerBanner, isVisible]) => { @@ -123,6 +135,7 @@ export class ChromeService { 'kbnBody', headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner', isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden', + getKbnVersionClass(), ]; }) ); diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 1c4e78f0a5c2e..8ead0f50785bd 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -46,6 +46,7 @@ const defaultCoreSystemParams = { csp: { warnLegacyBrowsers: true, }, + version: 'version', } as any, }; @@ -91,12 +92,12 @@ describe('constructor', () => { }); }); - it('passes browserSupportsCsp to ChromeService', () => { + it('passes browserSupportsCsp and coreContext to ChromeService', () => { createCoreSystem(); - expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1); expect(ChromeServiceConstructor).toHaveBeenCalledWith({ - browserSupportsCsp: expect.any(Boolean), + browserSupportsCsp: true, + kibanaVersion: 'version', }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index f0ea1e62fc33f..9a28bf45df927 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; import { CoreSetup, CoreStart } from '.'; @@ -98,6 +97,7 @@ export class CoreSystem { this.injectedMetadata = new InjectedMetadataService({ injectedMetadata, }); + this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.fatalErrors = new FatalErrorsService(rootDomElement, () => { // Stop Core before rendering any fatal errors into the DOM @@ -109,14 +109,16 @@ export class CoreSystem { this.savedObjects = new SavedObjectsService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); - this.chrome = new ChromeService({ browserSupportsCsp }); + this.chrome = new ChromeService({ + browserSupportsCsp, + kibanaVersion: injectedMetadata.version, + }); this.docLinks = new DocLinksService(); this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); - this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 31e85341fb519..ca95b253f9cdb 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1632,6 +1632,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` From a3286bbbd74d33c41e59a07089f08004c54d4142 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 23 Jun 2021 10:43:52 -0600 Subject: [PATCH 113/118] [Maps] show radius when drawing distance filter (#102808) (#103104) * [Maps] show radius when drawing distance filter * show more precision when radius is between 10km and 1km * move radius display from line to left of cursor Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../mb_map/draw_control/draw_circle.ts | 66 ++++++++++++++++--- .../mb_map/draw_control/draw_control.tsx | 24 ++++++- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts index f0df797582bef..998329a78bfbb 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts @@ -11,7 +11,11 @@ import turfDistance from '@turf/distance'; // @ts-expect-error import turfCircle from '@turf/circle'; -import { Position } from 'geojson'; +import { Feature, GeoJSON, Position } from 'geojson'; + +const DRAW_CIRCLE_RADIUS = 'draw-circle-radius'; + +export const DRAW_CIRCLE_RADIUS_MB_FILTER = ['==', 'meta', DRAW_CIRCLE_RADIUS]; export interface DrawCircleProperties { center: Position; @@ -22,10 +26,12 @@ type DrawCircleState = { circle: { properties: Omit & { center: Position | null; + edge: Position | null; + radiusKm: number; }; id: string | number; incomingCoords: (coords: unknown[]) => void; - toGeoJSON: () => unknown; + toGeoJSON: () => GeoJSON; }; }; @@ -43,6 +49,7 @@ export const DrawCircle = { type: 'Feature', properties: { center: null, + edge: null, radiusKm: 0, }, geometry: { @@ -96,6 +103,7 @@ export const DrawCircle = { } const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; + state.circle.properties.edge = mouseLocation; state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation); const newCircleFeature = turfCircle( state.circle.properties.center, @@ -124,15 +132,53 @@ export const DrawCircle = { this.changeMode('simple_select', {}, { silent: true }); } }, - toDisplayFeatures( - state: DrawCircleState, - geojson: { properties: { active: string } }, - display: (geojson: unknown) => unknown - ) { - if (state.circle.properties.center) { - geojson.properties.active = 'true'; - return display(geojson); + toDisplayFeatures(state: DrawCircleState, geojson: Feature, display: (geojson: Feature) => void) { + if (!state.circle.properties.center || !state.circle.properties.edge) { + return null; + } + + geojson.properties!.active = 'true'; + + let radiusLabel = ''; + if (state.circle.properties.radiusKm <= 1) { + radiusLabel = `${Math.round(state.circle.properties.radiusKm * 1000)} m`; + } else if (state.circle.properties.radiusKm <= 10) { + radiusLabel = `${state.circle.properties.radiusKm.toFixed(1)} km`; + } else { + radiusLabel = `${Math.round(state.circle.properties.radiusKm)} km`; } + + // display radius label, requires custom 'symbol' style with DRAW_CIRCLE_RADIUS_MB_FILTER filter + display({ + type: 'Feature', + properties: { + meta: DRAW_CIRCLE_RADIUS, + parent: state.circle.id, + radiusLabel, + active: 'false', + }, + geometry: { + type: 'Point', + coordinates: state.circle.properties.edge, + }, + }); + + // display line from center vertex to edge + display({ + type: 'Feature', + properties: { + meta: 'draw-circle-radius-line', + parent: state.circle.id, + active: 'true', + }, + geometry: { + type: 'LineString', + coordinates: [state.circle.properties.center, state.circle.properties.edge], + }, + }); + + // display circle + display(geojson); }, onTrash(state: DrawCircleState) { // @ts-ignore diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index 879bd85dd6019..5d9cb59bbe522 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -14,9 +14,11 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { Feature } from 'geojson'; import { DRAW_SHAPE } from '../../../../common/constants'; -import { DrawCircle } from './draw_circle'; +import { DrawCircle, DRAW_CIRCLE_RADIUS_MB_FILTER } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; +const GL_DRAW_RADIUS_LABEL_LAYER_ID = 'gl-draw-radius-label'; + const mbModeEquivalencies = new Map([ ['simple_select', DRAW_SHAPE.SIMPLE_SELECT], ['draw_rectangle', DRAW_SHAPE.BOUNDS], @@ -94,6 +96,7 @@ export class DrawControl extends Component { this.props.mbMap.getCanvas().style.cursor = ''; this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); + this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID); this.props.mbMap.removeControl(this._mbDrawControl); this._mbDrawControlAdded = false; } @@ -105,6 +108,25 @@ export class DrawControl extends Component { if (!this._mbDrawControlAdded) { this.props.mbMap.addControl(this._mbDrawControl); + this.props.mbMap.addLayer({ + id: GL_DRAW_RADIUS_LABEL_LAYER_ID, + type: 'symbol', + source: 'mapbox-gl-draw-hot', + filter: DRAW_CIRCLE_RADIUS_MB_FILTER, + layout: { + 'text-anchor': 'right', + 'text-field': '{radiusLabel}', + 'text-size': 16, + 'text-offset': [-1, 0], + 'text-ignore-placement': true, + 'text-allow-overlap': true, + }, + paint: { + 'text-color': '#fbb03b', + 'text-halo-color': 'rgba(255, 255, 255, 1)', + 'text-halo-width': 2, + }, + }); this._mbDrawControlAdded = true; this.props.mbMap.getCanvas().style.cursor = 'crosshair'; this.props.mbMap.on('draw.modechange', this._onModeChange); From 9abcf6e1e8e9407d551be9198a93f14592521515 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:47:31 -0400 Subject: [PATCH 114/118] [Discover] Improve flaky test - doc navigation (#102859) (#103108) * [Discover] test flakiness * [Discover] wait for doc loaded * [Discover] update related test * [Discover] clean statement Co-authored-by: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> --- test/functional/apps/discover/_data_grid_doc_navigation.ts | 6 ++++-- test/functional/apps/discover/_doc_navigation.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts index e3e8a20b693f8..cf5532aa6d762 100644 --- a/test/functional/apps/discover/_data_grid_doc_navigation.ts +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -41,8 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[0].click(); }); - const hasDocHit = await testSubjects.exists('doc-hit'); - expect(hasDocHit).to.be(true); + await retry.waitFor('hit loaded', async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + return !!hasDocHit; + }); }); // no longer relevant as null field won't be returned in the Fields API response diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts index 771dac4d40a64..8d156cb305586 100644 --- a/test/functional/apps/discover/_doc_navigation.ts +++ b/test/functional/apps/discover/_doc_navigation.ts @@ -51,8 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[1].click(); }); - const hasDocHit = await testSubjects.exists('doc-hit'); - expect(hasDocHit).to.be(true); + await retry.waitFor('hit loaded', async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + return !!hasDocHit; + }); }); // no longer relevant as null field won't be returned in the Fields API response From d063e684d7065ab5f3377b3c49fd5df7ee309a31 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:50:28 -0400 Subject: [PATCH 115/118] [Maps] remove undefined from map embeddable by_value URL (#102949) (#103109) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nathan Reese --- x-pack/plugins/maps/common/constants.ts | 11 +++++------ .../plugins/maps/public/embeddable/map_embeddable.tsx | 10 +++++----- .../maps/public/routes/map_page/map_app/map_app.tsx | 4 ++-- x-pack/plugins/maps/server/plugin.ts | 8 ++++---- x-pack/plugins/maps/server/saved_objects/map.ts | 4 ++-- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 37a8e8063c4ed..fa065e701184e 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -58,15 +58,14 @@ export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__'; export const MVT_TOKEN_PARAM_NAME = 'token'; -const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`; export function getNewMapPath() { - return MAP_BASE_URL; + return `/${MAPS_APP_PATH}/${MAP_PATH}`; } -export function getExistingMapPath(id: string) { - return `${MAP_BASE_URL}/${id}`; +export function getFullPath(id: string | undefined) { + return `/${MAPS_APP_PATH}${getEditPath(id)}`; } -export function getEditPath(id: string) { - return `/${MAP_PATH}/${id}`; +export function getEditPath(id: string | undefined) { + return id ? `/${MAP_PATH}/${id}` : `/${MAP_PATH}`; } export enum LAYER_TYPE { diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 5a477754683e6..509cece671dd6 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -54,9 +54,9 @@ import { } from '../selectors/map_selectors'; import { APP_ID, - getExistingMapPath, + getEditPath, + getFullPath, MAP_SAVED_OBJECT_TYPE, - MAP_PATH, RawValue, } from '../../common/constants'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; @@ -180,13 +180,13 @@ export class MapEmbeddable : ''; const input = this.getInput(); const title = input.hidePanelTitles ? '' : input.title || savedMapTitle; - const savedObjectId = (input as MapByReferenceInput).savedObjectId; + const savedObjectId = 'savedObjectId' in input ? input.savedObjectId : undefined; this.updateOutput({ ...this.getOutput(), defaultTitle: savedMapTitle, title, - editPath: `/${MAP_PATH}/${savedObjectId}`, - editUrl: getHttp().basePath.prepend(getExistingMapPath(savedObjectId)), + editPath: getEditPath(savedObjectId), + editUrl: getHttp().basePath.prepend(getFullPath(savedObjectId)), indexPatterns: await this._getIndexPatterns(), }); } diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 0dfff5a2c221e..92459ed28ab91 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -44,7 +44,7 @@ import { getTopNavConfig } from '../top_nav_config'; import { MapQuery } from '../../../../common/descriptor_types'; import { goToSpecifiedPath } from '../../../render_app'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { getExistingMapPath, APP_ID } from '../../../../common/constants'; +import { getFullPath, APP_ID } from '../../../../common/constants'; import { getInitialQuery, getInitialRefreshConfig, @@ -356,7 +356,7 @@ export class MapApp extends React.Component { const savedObjectId = this.props.savedMap.getSavedObjectId(); if (savedObjectId) { getCoreChrome().recentlyAccessed.add( - getExistingMapPath(savedObjectId), + getFullPath(savedObjectId), this.props.savedMap.getTitle(), savedObjectId ); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index c753297932037..b8676559a4e2b 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -22,7 +22,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; // @ts-ignore import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; -import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants'; +import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getFullPath } from '../common/constants'; import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore @@ -77,7 +77,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('ecommerce', [ { - path: getExistingMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), + path: getFullPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -99,7 +99,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('flights', [ { - path: getExistingMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), + path: getFullPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -120,7 +120,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects()); home.sampleData.addAppLinksToSampleDataset('logs', [ { - path: getExistingMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), + path: getFullPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts index 78f70e27b2b7b..24effd651a31b 100644 --- a/x-pack/plugins/maps/server/saved_objects/map.ts +++ b/x-pack/plugins/maps/server/saved_objects/map.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { APP_ICON, getExistingMapPath } from '../../common/constants'; +import { APP_ICON, getFullPath } from '../../common/constants'; // @ts-ignore import { savedObjectMigrations } from './saved_object_migrations'; @@ -34,7 +34,7 @@ export const mapSavedObjects: SavedObjectsType = { }, getInAppUrl(obj) { return { - path: getExistingMapPath(obj.id), + path: getFullPath(obj.id), uiCapabilitiesPath: 'maps.show', }; }, From 3d69df2d32acb6f92c3d0385f12741b220dcc988 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 23 Jun 2021 19:57:39 +0300 Subject: [PATCH 116/118] [Visualize] Adds an info icon tip to the update button (#101469) (#103112) * [Visualize] Adds an info tooltip to the update button * Add iconTip to the visEditor update button * Move to the left and change the icon * Update test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/sidebar/controls.tsx | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx index a24673a4c1245..e757b5fe8f61d 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx @@ -7,7 +7,14 @@ */ import React, { useCallback, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiToolTip, + EuiIconTip, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import useDebounce from 'react-use/lib/useDebounce'; @@ -84,19 +91,32 @@ function DefaultEditorControls({ ) : ( - - - + + + + + + + + + + )} From eea0addbb7ae2d768a8b82cf0e54251f36ece600 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:59:02 -0400 Subject: [PATCH 117/118] [Discover] Unskip and improve empty results query functional test (#102995) (#103110) Co-authored-by: Matthias Wilhelm --- test/functional/apps/discover/_discover.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 53682cdb2bf30..75bf9372d4d49 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -185,8 +185,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/89550 - describe.skip('query #2, which has an empty time range', () => { + describe('query #2, which has an empty time range', () => { const fromTime = 'Jun 11, 1999 @ 09:22:11.000'; const toTime = 'Jun 12, 1999 @ 11:21:04.000'; @@ -197,8 +196,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show "no results"', async () => { - const isVisible = await PageObjects.discover.hasNoResults(); - expect(isVisible).to.be(true); + await retry.waitFor('no results screen is displayed', async function () { + const isVisible = await PageObjects.discover.hasNoResults(); + return isVisible === true; + }); }); it('should suggest a new time range is picked', async () => { From b859c36ef666be3212ba26ecc4556593449e602e Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 23 Jun 2021 19:21:24 +0200 Subject: [PATCH 118/118] [Discover] Dont trigger onChange when the same index pattern is selected (#102652) (#103116) --- .../sidebar/change_indexpattern.test.tsx | 71 +++++++++++++++++++ .../sidebar/change_indexpattern.tsx | 12 ++-- 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx new file mode 100644 index 0000000000000..8c32942740a76 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { EuiSelectable } from '@elastic/eui'; +import { ShallowWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { shallowWithIntl } from '@kbn/test/jest'; +import { ChangeIndexPattern } from './change_indexpattern'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield'; +import { IndexPatternRef } from './types'; + +function getProps() { + return { + indexPatternId: indexPatternMock.id, + indexPatternRefs: [ + indexPatternMock as IndexPatternRef, + indexPatternWithTimefieldMock as IndexPatternRef, + ], + onChangeIndexPattern: jest.fn(), + trigger: { + label: indexPatternMock.title, + title: indexPatternMock.title, + 'data-test-subj': 'indexPattern-switch-link', + }, + }; +} + +function getIndexPatternPickerList(instance: ShallowWrapper) { + return instance.find(EuiSelectable).first(); +} + +function getIndexPatternPickerOptions(instance: ShallowWrapper) { + return getIndexPatternPickerList(instance).prop('options'); +} + +export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { + const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( + instance + ).map((option: { label: string }) => + option.label === selectedLabel + ? { ...option, checked: 'on' } + : { ...option, checked: undefined } + ); + return getIndexPatternPickerList(instance).prop('onChange')!(options); +} + +describe('ChangeIndexPattern', () => { + test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => { + const props = getProps(); + const comp = shallowWithIntl(); + await act(async () => { + selectIndexPatternPickerOption(comp, indexPatternMock.title); + }); + expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0); + }); + test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => { + const props = getProps(); + const comp = shallowWithIntl(); + await act(async () => { + selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title); + }); + expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1); + expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx index d5076e4daa990..5f2f35e2419dd 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx @@ -26,17 +26,17 @@ export type ChangeIndexPatternTriggerProps = EuiButtonProps & { // TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern export function ChangeIndexPattern({ - indexPatternRefs, indexPatternId, + indexPatternRefs, onChangeIndexPattern, - trigger, selectableProps, + trigger, }: { - trigger: ChangeIndexPatternTriggerProps; + indexPatternId?: string; indexPatternRefs: IndexPatternRef[]; onChangeIndexPattern: (newId: string) => void; - indexPatternId?: string; selectableProps?: EuiSelectableProps<{ value: string }>; + trigger: ChangeIndexPatternTriggerProps; }) { const [isPopoverOpen, setPopoverIsOpen] = useState(false); @@ -86,7 +86,9 @@ export function ChangeIndexPattern({ const choice = (choices.find(({ checked }) => checked) as unknown) as { value: string; }; - onChangeIndexPattern(choice.value); + if (choice.value !== indexPatternId) { + onChangeIndexPattern(choice.value); + } setPopoverIsOpen(false); }} searchProps={{