From 90dc176a74987c5b84cc44532c034f3002fb15bc Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 22 Oct 2020 18:41:17 +0200 Subject: [PATCH 01/15] upgrades cypress to version 5.4.0 (#81313) (#81465) --- x-pack/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index ab45805186b72..c205f8520ffc4 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -152,7 +152,7 @@ "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", "cronstrue": "^1.51.0", - "cypress": "^5.0.0", + "cypress": "5.4.0", "cypress-multi-reporters": "^1.2.3", "cypress-promise": "^1.1.0", "d3": "3.5.17", diff --git a/yarn.lock b/yarn.lock index 0d3a389b63bd6..424ae8f91d1d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9841,10 +9841,10 @@ cypress-promise@^1.1.0: resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" integrity sha512-DhIf5PJ/a0iY+Yii6n7Rbwq+9TJxU4pupXYzf9mZd8nPG0AzQrj9i+pqINv4xbI2EV1p+PKW3maCkR7oPG4GrA== -cypress@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.2.0.tgz#6902efd90703242a2539f0623c6e1118aff01f95" - integrity sha512-9S2spcrpIXrQ+CQIKHsjRoLQyRc2ehB06clJXPXXp1zyOL/uZMM3Qc20ipNki4CcNwY0nBTQZffPbRpODeGYQg== +cypress@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.4.0.tgz#8833a76e91129add601f823d43c53eb512d162c5" + integrity sha512-BJR+u3DRSYMqaBS1a3l1rbh5AkMRHugbxcYYzkl+xYlO6dzcJVE8uAhghzVI/hxijCyBg1iuSe4TRp/g1PUg8Q== dependencies: "@cypress/listr-verbose-renderer" "^0.4.1" "@cypress/request" "^2.88.5" From 57c31e8cd339f8fcdd5b2bc51143e787d0ba8870 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 22 Oct 2020 18:58:28 +0200 Subject: [PATCH 02/15] fix flaky unit test (#81466) (#81496) --- .../embeddable/embeddable.test.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index d91865c21a2a6..3e05d4ddfbc20 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -28,6 +28,7 @@ import { IBasePath } from '../../../../../../src/core/public'; import { AttributeService } from '../../../../../../src/plugins/embeddable/public'; import { LensAttributeService } from '../../lens_attribute_service'; import { OnSaveProps } from '../../../../../../src/plugins/saved_objects/public/save_modal'; +import { act } from 'react-dom/test-utils'; jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, @@ -337,10 +338,12 @@ describe('embeddable', () => { } as LensEmbeddableInput); embeddable.render(mountpoint); - embeddable.updateInput({ - timeRange, - query, - filters: [{ meta: { alias: 'test', negate: true, disabled: true } }], + act(() => { + embeddable.updateInput({ + timeRange, + query, + filters: [{ meta: { alias: 'test', negate: true, disabled: true } }], + }); }); expect(expressionRenderer).toHaveBeenCalledTimes(1); @@ -384,7 +387,9 @@ describe('embeddable', () => { } as LensEmbeddableInput); embeddable.render(mountpoint); - autoRefreshFetchSubject.next(); + act(() => { + autoRefreshFetchSubject.next(); + }); expect(expressionRenderer).toHaveBeenCalledTimes(2); }); From 3ed7ac79e5042a81511ed43a8dc0164819101415 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 22 Oct 2020 13:38:11 -0400 Subject: [PATCH 03/15] [task manager] do not sort tasks to be claimed by score if no pinned tasks (#80692) (#81495) resolves: https://github.com/elastic/kibana/issues/80371 Previously, when claiming tasks, we were always sorting the tasks to claim by the score and then by the time they should be run. We sort by score to capture `runNow()` tasks, also referred to internally as "pinned" tasks in the update by query. The change in this PR is to only sort by score if there are pinned tasks, and to not sort by score at all if there aren't any. --- .../task_manager/server/task_store.test.ts | 21 ++++++++++++++++++- .../plugins/task_manager/server/task_store.ts | 19 +++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 8d47d3dd30b82..a40df3b84132e 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -367,7 +367,7 @@ describe('TaskStore', () => { const { args: { - updateByQuery: { body: { query } = {} }, + updateByQuery: { body: { query, sort } = {} }, }, } = await testClaimAvailableTasks({ opts: { @@ -476,6 +476,25 @@ describe('TaskStore', () => { ], }, }); + expect(sort).toMatchObject([ + { + _script: { + type: 'number', + order: 'asc', + script: { + lang: 'painless', + source: ` +if (doc['task.retryAt'].size()!=0) { + return doc['task.retryAt'].value.toInstant().toEpochMilli(); +} +if (doc['task.runAt'].size()!=0) { + return doc['task.runAt'].value.toInstant().toEpochMilli(); +} + `, + }, + }, + }, + ]); }); test('it supports claiming specific tasks by id', async () => { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 4c41be9577ad0..63b6ab7412ec5 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -46,6 +46,7 @@ import { RangeFilter, asPinnedQuery, matchesClauses, + SortOptions, } from './queries/query_clauses'; import { @@ -272,6 +273,17 @@ export class TaskStore { ) ); + // The documents should be sorted by runAt/retryAt, unless there are pinned + // tasks being queried, in which case we want to sort by score first, and then + // the runAt/retryAt. That way we'll get the pinned tasks first. Note that + // the score seems to favor newer documents rather than older documents, so + // if there are not pinned tasks being queried, we do NOT want to sort by score + // at all, just by runAt/retryAt. + const sort: SortOptions = [SortByRunAtAndRetryAt]; + if (claimTasksById && claimTasksById.length) { + sort.unshift('_score'); + } + const apmTrans = apm.startTransaction(`taskManager markAvailableTasksAsClaimed`, 'taskManager'); const { updated } = await this.updateByQuery( asUpdateByQuery({ @@ -288,12 +300,7 @@ export class TaskStore { status: 'claiming', retryAt: claimOwnershipUntil, }), - sort: [ - // sort by score first, so the "pinned" Tasks are first - '_score', - // the nsort by other fields - SortByRunAtAndRetryAt, - ], + sort, }), { max_docs: size, From e831c929b368387bf2f909c765da25a3dd57fc18 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 22 Oct 2020 10:40:39 -0700 Subject: [PATCH 04/15] [Enterprise Search] Overview - add missing setup guide CTAs (#81420) (#81502) * Misc enterprise search overview cleanup - remove extra comment block - tests - use setMockValues - remove unused EuiPage - error connecting test - clarify assertion * Add Setup Guide CTA component * Update Product Selector to show Setup Guide CTAs + tests clean up - use setMockValues, import reorder * Add EuiPanel RR link to setup guide --- .../product_selector.test.tsx | 14 ++++--- .../product_selector/product_selector.tsx | 17 +++++---- .../components/setup_guide/index.ts | 1 + .../setup_guide/setup_guide_cta.scss | 29 ++++++++++++++ .../setup_guide/setup_guide_cta.test.tsx | 19 ++++++++++ .../setup_guide/setup_guide_cta.tsx | 38 +++++++++++++++++++ .../enterprise_search/index.test.tsx | 22 +++++++---- .../applications/enterprise_search/index.tsx | 2 +- .../react_router_helpers/eui_link.test.tsx | 13 +++++-- .../shared/react_router_helpers/eui_link.tsx | 24 ++++++++++-- .../shared/react_router_helpers/index.ts | 7 +++- 11 files changed, 154 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index f1f16d1a6f7a4..f2bdc1a8c75b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -4,28 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/kea.mock'; +import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; -import { ProductSelector } from './'; +import { SetupGuideCta } from '../setup_guide'; import { ProductCard } from '../product_card'; +import { ProductSelector } from './'; + describe('ProductSelector', () => { - it('renders the overview page and product cards with no host set', () => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + it('renders the overview page, product cards, & setup guide CTAs with no host set', () => { + setMockValues({ config: { host: '' } }); const wrapper = shallow(); expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); expect(wrapper.find(ProductCard)).toHaveLength(2); + expect(wrapper.find(SetupGuideCta)).toHaveLength(1); }); describe('access checks when host is set', () => { beforeEach(() => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); + setMockValues({ config: { host: 'localhost' } }); }); it('does not render the App Search card if the user does not have access to AS', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 6d76b741d7a97..235ececd8b6fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -3,11 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ import React from 'react'; import { useValues } from 'kea'; @@ -30,6 +25,7 @@ import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kiba import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { ProductCard } from '../product_card'; +import { SetupGuideCta } from '../setup_guide'; import AppSearchImage from '../../assets/app_search.png'; import WorkplaceSearchImage from '../../assets/workplace_search.png'; @@ -66,9 +62,13 @@ export const ProductSelector: React.FC = ({ access }) =>

- {i18n.translate('xpack.enterpriseSearch.overview.subheading', { - defaultMessage: 'Select a product to get started', - })} + {config.host + ? i18n.translate('xpack.enterpriseSearch.overview.subheading', { + defaultMessage: 'Select a product to get started.', + }) + : i18n.translate('xpack.enterpriseSearch.overview.setupHeading', { + defaultMessage: 'Choose a product to set up and get started.', + })}

@@ -87,6 +87,7 @@ export const ProductSelector: React.FC = ({ access }) => )} + {!config.host && } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts index c367424d375f9..89f7da4547569 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts @@ -5,3 +5,4 @@ */ export { SetupGuide } from './setup_guide'; +export { SetupGuideCta } from './setup_guide_cta'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss new file mode 100644 index 0000000000000..103ef8eccb558 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.scss @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +.enterpriseSearchSetupCta { + margin: $euiSize auto $euiSizeXL; + + // Clickable EuiPanel override - line panel up with product cards + &.euiPanel--isClickable { + width: calc(100% - #{$euiSize}); + } + + &__text { + max-width: $euiSize * 40; + } + + &__image { + display: block; + max-width: 100%; + width: $euiSize * 10; + margin: 0 auto; + + @include euiBreakpoint('xs', 's') { + width: $euiSize * 15; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx new file mode 100644 index 0000000000000..f235beef3b337 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SetupGuideCta } from './'; + +describe('SetupGuideCta', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('.enterpriseSearchSetupCta')).toHaveLength(1); + expect(wrapper.find('img')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx new file mode 100644 index 0000000000000..2a0e2ffc34f3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; +import { EuiPanel } from '../../../shared/react_router_helpers'; + +import CtaImage from './assets/getting_started.png'; +import './setup_guide_cta.scss'; + +export const SetupGuideCta: React.FC = () => ( + + + + +

+ {i18n.translate('xpack.enterpriseSearch.overview.setupCta.title', { + defaultMessage: 'Enterprise-grade functionality for teams big and small', + })} +

+
+ + {i18n.translate('xpack.enterpriseSearch.overview.setupCta.description', { + defaultMessage: + 'Add search to your app or internal organization with Elastic App Search and Workplace Search. Watch the video to see what you can do when search is made easy.', + })} + +
+ + + +
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index 803d2c8462b1b..0e929c9191e0f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -6,10 +6,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPage } from '@elastic/eui'; -import '../__mocks__/kea.mock'; -import { useValues } from 'kea'; +import { setMockValues } from '../__mocks__/kea.mock'; import { EnterpriseSearch } from './'; import { SetupGuide } from './components/setup_guide'; @@ -18,7 +16,7 @@ import { ProductSelector } from './components/product_selector'; describe('EnterpriseSearch', () => { it('renders the Setup Guide and Product Selector', () => { - (useValues as jest.Mock).mockReturnValue({ + setMockValues({ errorConnecting: false, config: { host: 'localhost' }, }); @@ -28,15 +26,23 @@ describe('EnterpriseSearch', () => { expect(wrapper.find(ProductSelector)).toHaveLength(1); }); - it('renders the error connecting prompt when host is not configured', () => { - (useValues as jest.Mock).mockReturnValueOnce({ + it('renders the error connecting prompt only if host is configured', () => { + setMockValues({ errorConnecting: true, - config: { host: '' }, + config: { host: 'localhost' }, }); const wrapper = shallow(); expect(wrapper.find(ErrorConnecting)).toHaveLength(1); - expect(wrapper.find(EuiPage)).toHaveLength(0); expect(wrapper.find(ProductSelector)).toHaveLength(0); + + setMockValues({ + errorConnecting: true, + config: { host: '' }, + }); + wrapper.setProps({}); // Re-render + + expect(wrapper.find(ErrorConnecting)).toHaveLength(0); + expect(wrapper.find(ProductSelector)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 7b97c6c9e58b6..048baabe6a1dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -25,7 +25,7 @@ export const EnterpriseSearch: React.FC = ({ access = {} }) => const { errorConnecting } = useValues(HttpLogic); const { config } = useValues(KibanaLogic); - const showErrorConnecting = config.host && errorConnecting; + const showErrorConnecting = !!(config.host && errorConnecting); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx index 82fbb8940d460..3a4585b6d9a71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -8,18 +8,18 @@ import '../../__mocks__/kea.mock'; import React from 'react'; import { shallow, mount } from 'enzyme'; -import { EuiLink, EuiButton } from '@elastic/eui'; +import { EuiLink, EuiButton, EuiPanel } from '@elastic/eui'; import { mockKibanaValues, mockHistory } from '../../__mocks__'; -import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link'; +import { EuiReactRouterLink, EuiReactRouterButton, EuiReactRouterPanel } from './eui_link'; describe('EUI & React Router Component Helpers', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('renders', () => { + it('renders an EuiLink', () => { const wrapper = shallow(); expect(wrapper.find(EuiLink)).toHaveLength(1); @@ -31,6 +31,13 @@ describe('EUI & React Router Component Helpers', () => { expect(wrapper.find(EuiButton)).toHaveLength(1); }); + it('renders an EuiPanel', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiPanel)).toHaveLength(1); + expect(wrapper.find(EuiPanel).prop('paddingSize')).toEqual('l'); + }); + it('passes down all ...rest props', () => { const wrapper = shallow(); const link = wrapper.find(EuiLink); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx index f9f6ec54e8832..78546911813ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -6,14 +6,15 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui'; +import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps, EuiPanel } from '@elastic/eui'; +import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel'; import { KibanaLogic } from '../kibana'; import { HttpLogic } from '../http'; import { letBrowserHandleEvent, createHref } from './'; /** - * Generates either an EuiLink or EuiButton with a React-Router-ified link + * Generates EUI components with React-Router-ified links * * Based off of EUI's recommendations for handling React Router: * https://github.com/elastic/eui/blob/master/wiki/react-router.md#react-router-51 @@ -54,9 +55,11 @@ export const EuiReactRouterHelper: React.FC = ({ return React.cloneElement(children as React.ReactElement, reactRouterProps); }; -type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps; -type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; +/** + * Component helpers + */ +type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps; export const EuiReactRouterLink: React.FC = ({ to, onClick, @@ -68,6 +71,7 @@ export const EuiReactRouterLink: React.FC = ({ ); +type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; export const EuiReactRouterButton: React.FC = ({ to, onClick, @@ -78,3 +82,15 @@ export const EuiReactRouterButton: React.FC = ({ ); + +type TEuiReactRouterPanelProps = EuiPanelProps & IEuiReactRouterProps; +export const EuiReactRouterPanel: React.FC = ({ + to, + onClick, + shouldNotCreateHref, + ...rest +}) => ( + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts index 6915d3222c45c..36fb0560d7323 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts @@ -6,5 +6,8 @@ export { letBrowserHandleEvent } from './link_events'; export { createHref, ICreateHrefOptions } from './create_href'; -export { EuiReactRouterLink as EuiLink } from './eui_link'; -export { EuiReactRouterButton as EuiButton } from './eui_link'; +export { + EuiReactRouterLink as EuiLink, + EuiReactRouterButton as EuiButton, + EuiReactRouterPanel as EuiPanel, +} from './eui_link'; From 2937c2a3e0e328ee182901e5727a6889b765a2ef Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 22 Oct 2020 12:45:52 -0700 Subject: [PATCH 05/15] [App Search] Credentials: add FlashMessages, stub out credentials flyout (#81391) (#81517) * Added an empty Flyout * Refactor CredentialsFlyout to its own component folder + split out child sub components for easier testing/reading * Add initial FlashMessages setup - mostly just DELETE_MESSAGE currently, since that's what's already wired up - CREATE_MESSAGE and UPDATE_MESSAGE will be used in an upcoming commit + adds FlashMessages in flyout, which will show returned form errors from the API * Fix flash messages appearing on flyout open e.g. deletion success messages + incidental linting/cleanup Co-authored-by: Jason Stoltzfus Co-authored-by: Jason Stoltzfus --- .../components/credentials/constants.ts | 12 ++++ .../credentials/credentials.test.tsx | 13 ++++ .../components/credentials/credentials.tsx | 9 ++- .../credentials_flyout/body.test.tsx | 18 ++++++ .../credentials/credentials_flyout/body.tsx | 19 ++++++ .../credentials_flyout/footer.test.tsx | 64 +++++++++++++++++++ .../credentials/credentials_flyout/footer.tsx | 54 ++++++++++++++++ .../credentials_flyout/header.test.tsx | 55 ++++++++++++++++ .../credentials/credentials_flyout/header.tsx | 34 ++++++++++ .../credentials_flyout/index.test.tsx | 33 ++++++++++ .../credentials/credentials_flyout/index.tsx | 35 ++++++++++ .../credentials/credentials_logic.test.ts | 16 ++++- .../credentials/credentials_logic.ts | 24 ++++--- 13 files changed, 373 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 374a2420f5ba7..53aa3db00b66a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -11,6 +11,16 @@ export enum ApiTokenTypes { Search = 'search', } +export const CREATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.created', { + defaultMessage: 'Successfully created key.', +}); +export const UPDATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.update', { + defaultMessage: 'Successfully updated API Key.', +}); +export const DELETE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.deleted', { + defaultMessage: 'Successfully deleted key.', +}); + export const SEARCH_DISPLAY = i18n.translate( 'xpack.enterpriseSearch.appSearch.tokens.permissions.display.search', { @@ -81,3 +91,5 @@ export const TOKEN_TYPE_INFO = [ { value: ApiTokenTypes.Private, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Private] }, { value: ApiTokenTypes.Admin, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Admin] }, ]; + +export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; 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 a265b2c998d39..a9a0dab044351 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 @@ -14,6 +14,7 @@ import { Credentials } from './credentials'; import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; import { externalUrl } from '../../../shared/enterprise_search_url'; +import { CredentialsFlyout } from './credentials_flyout'; describe('Credentials', () => { // Kea mocks @@ -71,4 +72,16 @@ describe('Credentials', () => { button.props().onClick(); expect(actions.showCredentialsForm).toHaveBeenCalledTimes(1); }); + + it('will render CredentialsFlyout if shouldShowCredentialsForm is true', () => { + setMockValues({ shouldShowCredentialsForm: true }); + const wrapper = shallow(); + expect(wrapper.find(CredentialsFlyout)).toHaveLength(1); + }); + + it('will NOT render CredentialsFlyout if shouldShowCredentialsForm is false', () => { + setMockValues({ shouldShowCredentialsForm: false }); + const wrapper = shallow(); + expect(wrapper.find(CredentialsFlyout)).toHaveLength(0); + }); }); 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 b9a482ae462d5..c8eae8cc13f5f 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 @@ -24,16 +24,19 @@ import { import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { FlashMessages } from '../../../shared/flash_messages'; + import { CredentialsLogic } from './credentials_logic'; import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; import { CredentialsList } from './credentials_list'; +import { CredentialsFlyout } from './credentials_flyout'; export const Credentials: React.FC = () => { const { initializeCredentialsData, resetCredentials, showCredentialsForm } = useActions( CredentialsLogic ); - const { dataLoading } = useValues(CredentialsLogic); + const { dataLoading, shouldShowCredentialsForm } = useValues(CredentialsLogic); useEffect(() => { initializeCredentialsData(); @@ -63,6 +66,7 @@ export const Credentials: React.FC = () => { + {shouldShowCredentialsForm && }

@@ -120,7 +124,8 @@ export const Credentials: React.FC = () => { )} - + + {!!dataLoading ? : } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx new file mode 100644 index 0000000000000..d2e7ff5f32dd4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyoutBody } from '@elastic/eui'; + +import { CredentialsFlyoutBody } from './body'; + +describe('CredentialsFlyoutBody', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFlyoutBody)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx new file mode 100644 index 0000000000000..2afba633ca892 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlyoutBody } from '@elastic/eui'; + +import { FlashMessages } from '../../../../shared/flash_messages'; + +export const CredentialsFlyoutBody: React.FC = () => { + return ( + + + Details go here + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx new file mode 100644 index 0000000000000..1ec3e4756c5c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyoutFooter, EuiButtonEmpty } from '@elastic/eui'; + +import { CredentialsFlyoutFooter } from './footer'; + +describe('CredentialsFlyoutFooter', () => { + const values = { + activeApiTokenExists: false, + }; + const actions = { + hideCredentialsForm: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFlyoutFooter)).toHaveLength(1); + }); + + it('closes the flyout', () => { + const wrapper = shallow(); + const button = wrapper.find(EuiButtonEmpty); + button.simulate('click'); + expect(button.prop('children')).toEqual('Close'); + expect(actions.hideCredentialsForm).toHaveBeenCalled(); + }); + + it('renders action button text for new tokens', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="APIKeyActionButton"]'); + + expect(button.prop('children')).toEqual('Save'); + }); + + it('renders action button text for existing tokens', () => { + setMockValues({ activeApiTokenExists: true }); + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="APIKeyActionButton"]'); + + expect(button.prop('children')).toEqual('Update'); + }); + + it('calls onApiTokenChange on action button press', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="APIKeyActionButton"]'); + button.simulate('click'); + + // TODO: Expect onApiTokenChange to have been called + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx new file mode 100644 index 0000000000000..7564560eade95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues, useActions } from 'kea'; +import { + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CredentialsLogic } from '../credentials_logic'; + +export const CredentialsFlyoutFooter: React.FC = () => { + const { hideCredentialsForm } = useActions(CredentialsLogic); + const { activeApiTokenExists } = useValues(CredentialsLogic); + + return ( + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.closeText', { + defaultMessage: 'Close', + })} + + + + window.alert('submit')} + fill={true} + color="secondary" + iconType="check" + data-test-subj="APIKeyActionButton" + > + {activeApiTokenExists + ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateText', { + defaultMessage: 'Update', + }) + : i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.saveText', { + defaultMessage: 'Save', + })} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx new file mode 100644 index 0000000000000..a8d9505136faa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyoutHeader } from '@elastic/eui'; + +import { ApiTokenTypes } from '../constants'; +import { IApiToken } from '../types'; + +import { CredentialsFlyoutHeader } from './header'; + +describe('CredentialsFlyoutHeader', () => { + const apiToken: IApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + }; + const values = { + activeApiToken: apiToken, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFlyoutHeader)).toHaveLength(1); + expect(wrapper.find('h2').prop('id')).toEqual('credentialsFlyoutTitle'); + expect(wrapper.find('h2').prop('children')).toEqual('Create a new key'); + }); + + it('changes the title text if editing an existing token', () => { + setMockValues({ + activeApiToken: { + ...apiToken, + id: 'some-id', + name: 'search-key', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find('h2').prop('children')).toEqual('Update search-key'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx new file mode 100644 index 0000000000000..f208cd1c5918f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; +import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CredentialsLogic } from '../credentials_logic'; +import { FLYOUT_ARIA_LABEL_ID } from '../constants'; + +export const CredentialsFlyoutHeader: React.FC = () => { + const { activeApiToken } = useValues(CredentialsLogic); + + return ( + + +

+ {activeApiToken.id + ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateTitle', { + defaultMessage: 'Update {tokenName}', + values: { tokenName: activeApiToken.name }, + }) + : i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.createTitle', { + defaultMessage: 'Create a new key', + })} +

+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx new file mode 100644 index 0000000000000..16b669c530012 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFlyout } from '@elastic/eui'; + +import { CredentialsFlyout } from './'; + +describe('CredentialsFlyout', () => { + const actions = { + hideCredentialsForm: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + const flyout = wrapper.find(EuiFlyout); + + expect(flyout).toHaveLength(1); + expect(flyout.prop('aria-labelledby')).toEqual('credentialsFlyoutTitle'); + expect(flyout.prop('onClose')).toEqual(actions.hideCredentialsForm); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx new file mode 100644 index 0000000000000..602a5250716c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useActions } from 'kea'; +import { EuiPortal, EuiFlyout } from '@elastic/eui'; + +import { CredentialsLogic } from '../credentials_logic'; +import { FLYOUT_ARIA_LABEL_ID } from '../constants'; +import { CredentialsFlyoutHeader } from './header'; +import { CredentialsFlyoutBody } from './body'; +import { CredentialsFlyoutFooter } from './footer'; + +export const CredentialsFlyout: React.FC = () => { + const { hideCredentialsForm } = useActions(CredentialsLogic); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 11b1253332cb2..8eb0f7582516d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -14,9 +14,15 @@ jest.mock('../../../shared/http', () => ({ })); import { HttpLogic } from '../../../shared/http'; jest.mock('../../../shared/flash_messages', () => ({ + FlashMessagesLogic: { actions: { clearFlashMessages: jest.fn() } }, + setSuccessMessage: jest.fn(), flashAPIErrors: jest.fn(), })); -import { flashAPIErrors } from '../../../shared/flash_messages'; +import { + FlashMessagesLogic, + setSuccessMessage, + flashAPIErrors, +} from '../../../shared/flash_messages'; describe('CredentialsLogic', () => { const DEFAULT_VALUES = { @@ -952,6 +958,13 @@ describe('CredentialsLogic', () => { }); }); }); + + describe('listener side-effects', () => { + it('should clear flashMessages whenever the credentials form flyout is opened', () => { + CredentialsLogic.actions.showCredentialsForm(); + expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled(); + }); + }); }); describe('hideCredentialsForm', () => { @@ -1142,6 +1155,7 @@ describe('CredentialsLogic', () => { ); await promise; expect(CredentialsLogic.actions.onApiKeyDelete).toHaveBeenCalledWith(tokenName); + expect(setSuccessMessage).toHaveBeenCalled(); }); it('handles errors', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index c6f929c45eb23..40966d64212f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -7,11 +7,16 @@ import { kea, MakeLogicType } from 'kea'; import { formatApiName } from '../../utils/format_api_name'; -import { ApiTokenTypes } from './constants'; +import { ApiTokenTypes, DELETE_MESSAGE } from './constants'; import { HttpLogic } from '../../../shared/http'; +import { + FlashMessagesLogic, + setSuccessMessage, + flashAPIErrors, +} from '../../../shared/flash_messages'; + import { IMeta } from '../../../../../common/types'; -import { flashAPIErrors } from '../../../shared/flash_messages'; import { IEngine } from '../../types'; import { IApiToken, ICredentialsDetails, ITokenReadWrite } from './types'; @@ -23,9 +28,7 @@ const defaultApiToken: IApiToken = { access_all_engines: true, }; -// TODO CREATE_MESSAGE, UPDATE_MESSAGE, and DELETE_MESSAGE from ent-search - -export interface ICredentialsLogicActions { +interface ICredentialsLogicActions { addEngineName(engineName: string): string; onApiKeyDelete(tokenName: string): string; onApiTokenCreateSuccess(apiToken: IApiToken): IApiToken; @@ -48,7 +51,7 @@ export interface ICredentialsLogicActions { deleteApiKey(tokenName: string): string; } -export interface ICredentialsLogicValues { +interface ICredentialsLogicValues { activeApiToken: IApiToken; activeApiTokenExists: boolean; activeApiTokenRawName: string; @@ -79,10 +82,7 @@ export const CredentialsLogic = kea< setCredentialsData: (meta, apiTokens) => ({ meta, apiTokens }), setCredentialsDetails: (details) => details, setNameInputBlurred: (nameInputBlurred) => nameInputBlurred, - setTokenReadWrite: ({ name, checked }) => ({ - name, - checked, - }), + setTokenReadWrite: ({ name, checked }) => ({ name, checked }), setTokenName: (name) => name, setTokenType: (tokenType) => tokenType, showCredentialsForm: (apiToken = { ...defaultApiToken }) => apiToken, @@ -217,6 +217,9 @@ export const CredentialsLogic = kea< ], }), listeners: ({ actions, values }) => ({ + showCredentialsForm: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, initializeCredentialsData: () => { actions.fetchCredentials(); actions.fetchDetails(); @@ -247,6 +250,7 @@ export const CredentialsLogic = kea< await http.delete(`/api/app_search/credentials/${tokenName}`); actions.onApiKeyDelete(tokenName); + setSuccessMessage(DELETE_MESSAGE); } catch (e) { flashAPIErrors(e); } From 8d0c2125c888dc05c1579e8c2b476a1f1dbe134b Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 22 Oct 2020 13:13:57 -0700 Subject: [PATCH 06/15] [Fleet] Show beta message without scrolling (#81416) (#81520) * Show beta message without scrolling * Update EUI var --- .../public/applications/ingest_manager/layouts/default.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index d71b90d16725d..5d39995922b93 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -18,7 +18,9 @@ interface Props { } const Container = styled.div` - min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); + min-height: calc( + 100vh - ${(props) => parseFloat(props.theme.eui.euiHeaderHeightCompensation) * 2}px + ); background: ${(props) => props.theme.eui.euiColorEmptyShade}; display: flex; flex-direction: column; From ae0bf633c68aa81f1745709c334fe4ed9e4708a3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 22 Oct 2020 21:41:24 +0100 Subject: [PATCH 07/15] add xpack.actions.proxyUrl setting (#81507) (#81528) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Julien Guay Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../os_packages/docker_generator/resources/bin/kibana-docker | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 577893eda1052..495960e3731b8 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -138,6 +138,7 @@ kibana_vars=( tilemap.url timelion.enabled vega.enableExternalUrls + xpack.actions.proxyUrl xpack.apm.enabled xpack.apm.serviceMapEnabled xpack.apm.ui.enabled From cbc7f42c7e53cf6b5684b200f88b331b6bfd2dc4 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 22 Oct 2020 16:37:13 -0600 Subject: [PATCH 08/15] [Maps] fix refreshing the page causes loss of unsaved change (#81226) (#81537) * [Maps] fix refreshing the page causes loss of unsaved change * fix functional tests * remove unneeded check for app leave modal * check for appLeaveConfirmModal for clicking confirmModalConfirmButton * see failures without closing appLeaveModal * start clearing unsaved changes warning in maps * clean up more unsaved state in functional tests * more cleaning up of unsaved state in functional tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/page_objects/common_page.ts | 7 ++++--- .../routing/routes/maps_app/get_breadcrumbs.tsx | 17 ++++++++++++----- .../routing/routes/maps_app/maps_app_view.tsx | 6 ++---- x-pack/test/functional/apps/maps/discover.js | 2 ++ x-pack/test/functional/apps/maps/joins.js | 1 + .../functional/apps/maps/layer_visibility.js | 1 + .../test/functional/apps/maps/vector_styling.js | 1 + x-pack/test/functional/page_objects/gis_page.ts | 8 ++++++++ 8 files changed, 31 insertions(+), 12 deletions(-) diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index ec33462eb361a..43ada726c2915 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -117,11 +117,12 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } else { log.debug(`navigateToUrl ${appUrl}`); await browser.get(appUrl, insertTimestamp); - // accept alert if it pops up - const alert = await browser.getAlert(); - await alert?.accept(); } + // accept alert if it pops up + const alert = await browser.getAlert(); + await alert?.accept(); + const currentUrl = shouldLoginIfPrompted ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx index 1f74b0d6d1449..d9b60b670b93e 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx @@ -5,17 +5,21 @@ */ import { i18n } from '@kbn/i18n'; -import { getNavigateToApp } from '../../../kibana_services'; +import { getCoreOverlays, getNavigateToApp } from '../../../kibana_services'; import { goToSpecifiedPath } from '../../maps_router'; import { getAppTitle } from '../../../../common/i18n_getters'; export const unsavedChangesWarning = i18n.translate( 'xpack.maps.breadCrumbs.unsavedChangesWarning', { - defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', + defaultMessage: 'Leave Maps with unsaved work?', } ); +export const unsavedChangesTitle = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesTitle', { + defaultMessage: 'Unsaved changes', +}); + export function getBreadcrumbs({ title, getHasUnsavedChanges, @@ -39,10 +43,13 @@ export function getBreadcrumbs({ breadcrumbs.push({ text: getAppTitle(), - onClick: () => { + onClick: async () => { if (getHasUnsavedChanges()) { - const navigateAway = window.confirm(unsavedChangesWarning); - if (navigateAway) { + const confirmed = await getCoreOverlays().openConfirm(unsavedChangesWarning, { + title: unsavedChangesTitle, + 'data-test-subj': 'appLeaveConfirmModal', + }); + if (confirmed) { goToSpecifiedPath('/'); } } else { diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx index bd08b2f11fadc..df46d5d6a13ff 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx @@ -43,7 +43,7 @@ import { import { MapContainer } from '../../../connected_components/map_container'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from './top_nav_config'; -import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs'; +import { getBreadcrumbs, unsavedChangesTitle, unsavedChangesWarning } from './get_breadcrumbs'; import { LayerDescriptor, MapRefreshConfig, @@ -138,9 +138,7 @@ export class MapsAppView extends React.Component { this.props.onAppLeave((actions) => { if (this._hasUnsavedChanges()) { - if (!window.confirm(unsavedChangesWarning)) { - return {} as AppLeaveAction; - } + return actions.confirm(unsavedChangesWarning, unsavedChangesTitle); } return actions.default() as AppLeaveAction; }); diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js index 8dbd98ed3af2f..6a2c1f8437698 100644 --- a/x-pack/test/functional/apps/maps/discover.js +++ b/x-pack/test/functional/apps/maps/discover.js @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }) { expect(doesLayerExist).to.equal(true); const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('4'); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); }); it('should link geo_point fields to Maps application with time and query context', async () => { @@ -55,6 +56,7 @@ export default function ({ getService, getPageObjects }) { expect(doesLayerExist).to.equal(true); const hits = await PageObjects.maps.getHits(); expect(hits).to.equal('7'); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); }); }); } diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index bd5ecfe2a2504..0e2850dafbccc 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -38,6 +38,7 @@ export default function ({ getPageObjects, getService }) { after(async () => { await inspector.close(); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); await security.testUser.restoreDefaults(); }); diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/layer_visibility.js index dd9b93c995695..75a0e7da0f256 100644 --- a/x-pack/test/functional/apps/maps/layer_visibility.js +++ b/x-pack/test/functional/apps/maps/layer_visibility.js @@ -19,6 +19,7 @@ export default function ({ getPageObjects, getService }) { afterEach(async () => { await inspector.close(); + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); await security.testUser.restoreDefaults(); }); diff --git a/x-pack/test/functional/apps/maps/vector_styling.js b/x-pack/test/functional/apps/maps/vector_styling.js index 1def542982dd8..e4c5eaf892c76 100644 --- a/x-pack/test/functional/apps/maps/vector_styling.js +++ b/x-pack/test/functional/apps/maps/vector_styling.js @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.maps.loadSavedMap('document example'); }); after(async () => { + await PageObjects.maps.refreshAndClearUnsavedChangesWarning(); await security.testUser.restoreDefaults(); }); diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index fdb0ea60ac3b0..247e743458817 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -19,6 +19,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const queryBar = getService('queryBar'); const comboBox = getService('comboBox'); const renderable = getService('renderable'); + const browser = getService('browser'); function escapeLayerName(layerName: string) { return layerName.split(' ').join('_'); @@ -692,6 +693,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } await testSubjects.click('mapSettingSubmitButton'); } + + async refreshAndClearUnsavedChangesWarning() { + await browser.refresh(); + // accept alert if it pops up + const alert = await browser.getAlert(); + await alert?.accept(); + } } return new GisPage(); } From f82cb3b3809ec9def79f6eb430be245187753815 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Thu, 22 Oct 2020 21:23:07 -0400 Subject: [PATCH 09/15] [7.x] [Security Solution] [Detections] Bug fix for rule status utility functions and updated tests and types (#81256) (#81545) * adds try catch in getFailingRules method and adds a test for merge statuses * rename IRuleStatusAttributes to IRuleStatusSOAttributes to better reflect what specifically the interface represents, which are the properties on the rule status saved object, while maintaining a separate interface for the HTTP response representation of those attributes as snake_case * adds tests for getFailingRules utility function * minor cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/detection_engine/routes/utils.test.ts | 141 +++++++++++++++++- .../lib/detection_engine/routes/utils.ts | 59 +++++--- .../lib/detection_engine/rules/types.ts | 24 ++- .../signals/__mocks__/es_results.ts | 10 +- .../signals/get_or_create_rule_statuses.ts | 6 +- .../signals/get_rule_status_saved_objects.ts | 4 +- .../rule_status_saved_objects_client.ts | 15 +- .../signals/rule_status_service.ts | 6 +- 8 files changed, 219 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index 6768e9534a87e..977dad680f8a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -7,7 +7,9 @@ import Boom from 'boom'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types'; + +import { alertsClientMock } from '../../../../../alerts/server/mocks'; +import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusSOAttributes } from '../rules/types'; import { BadRequestError } from '../errors/bad_request_error'; import { transformError, @@ -19,8 +21,14 @@ import { transformImportError, convertToSnakeCase, SiemResponseFactory, + mergeStatuses, + getFailingRules, } from './utils'; import { responseMock } from './__mocks__'; +import { exampleRuleStatus, exampleFindRuleStatusResponse } from '../signals/__mocks__/es_results'; +import { getResult } from './__mocks__/request_responses'; + +let alertsClient: ReturnType; describe('utils', () => { describe('transformError', () => { @@ -319,7 +327,7 @@ describe('utils', () => { saved_objects: [], }; expect( - convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not + convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not ).toEqual(null); }); }); @@ -350,4 +358,133 @@ describe('utils', () => { ); }); }); + + describe('mergeStatuses', () => { + it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => { + const statusOne = exampleRuleStatus(); + statusOne.attributes.status = 'failed'; + const statusTwo = exampleRuleStatus(); + statusTwo.attributes.status = 'failed'; + const currentStatus = exampleRuleStatus(); + const foundRules = exampleFindRuleStatusResponse([currentStatus, statusOne, statusTwo]); + const res = mergeStatuses(currentStatus.attributes.alertId, foundRules.saved_objects, { + 'myfakealertid-8cfac': { + current_status: { + alert_id: 'myfakealertid-8cfac', + status_date: '2020-03-27T22:55:59.517Z', + status: 'succeeded', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + failures: [], + }, + }); + expect(res).toEqual({ + 'myfakealertid-8cfac': { + current_status: { + alert_id: 'myfakealertid-8cfac', + status_date: '2020-03-27T22:55:59.517Z', + status: 'succeeded', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + failures: [], + }, + 'f4b8e31d-cf93-4bde-a265-298bde885cd7': { + current_status: { + alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + status_date: '2020-03-27T22:55:59.517Z', + status: 'succeeded', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + failures: [ + { + alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + status_date: '2020-03-27T22:55:59.517Z', + status: 'failed', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + { + alert_id: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + status_date: '2020-03-27T22:55:59.517Z', + status: 'failed', + last_failure_at: null, + last_success_at: '2020-03-27T22:55:59.517Z', + last_failure_message: null, + last_success_message: 'succeeded', + gap: null, + bulk_create_time_durations: [], + search_after_time_durations: [], + last_look_back_date: null, + }, + ], + }, + }); + }); + }); + + describe('getFailingRules', () => { + beforeEach(() => { + alertsClient = alertsClientMock.create(); + }); + it('getFailingRules finds no failing rules', async () => { + alertsClient.get.mockResolvedValue(getResult()); + const res = await getFailingRules(['my-fake-id'], alertsClient); + expect(res).toEqual({}); + }); + it('getFailingRules finds a failing rule', async () => { + const foundRule = getResult(); + foundRule.executionStatus = { + status: 'error', + lastExecutionDate: foundRule.executionStatus.lastExecutionDate, + error: { + reason: 'read', + message: 'oops', + }, + }; + alertsClient.get.mockResolvedValue(foundRule); + const res = await getFailingRules([foundRule.id], alertsClient); + expect(res).toEqual({ [foundRule.id]: foundRule }); + }); + it('getFailingRules throws an error', async () => { + alertsClient.get.mockImplementation(() => { + throw new Error('my test error'); + }); + let error; + try { + await getFailingRules(['my-fake-id'], alertsClient); + } catch (exc) { + error = exc; + } + expect(error.message).toEqual( + 'Failed to get executionStatus with AlertsClient: my test error' + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts index 96f96d7ebcc9e..72be7a3c0fa08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts @@ -17,7 +17,7 @@ import { } from '../../../../../../../src/core/server'; import { AlertsClient } from '../../../../../alerts/server'; import { BadRequestError } from '../errors/bad_request_error'; -import { RuleStatusResponse, IRuleStatusAttributes } from '../rules/types'; +import { RuleStatusResponse, IRuleStatusSOAttributes } from '../rules/types'; export interface OutputError { message: string; @@ -294,39 +294,53 @@ export const convertToSnakeCase = >( }, {}); }; +/** + * + * @param id rule id + * @param currentStatusAndFailures array of rule statuses where the 0th status is the current status and 1-5 positions are the historical failures + * @param acc accumulated rule id : statuses + */ export const mergeStatuses = ( id: string, - failures: Array>, + currentStatusAndFailures: Array>, acc: RuleStatusResponse -) => { - if (failures.length === 0) { +): RuleStatusResponse => { + if (currentStatusAndFailures.length === 0) { return { ...acc, }; } - const convertedCurrentStatus = convertToSnakeCase(failures[0].attributes); + const convertedCurrentStatus = convertToSnakeCase( + currentStatusAndFailures[0].attributes + ); return { ...acc, [id]: { current_status: convertedCurrentStatus, - failures: failures.map((errorItem) => - convertToSnakeCase(errorItem.attributes) - ), + failures: currentStatusAndFailures + .slice(1) + .map((errorItem) => convertToSnakeCase(errorItem.attributes)), }, } as RuleStatusResponse; }; -export const getFailingRules = (ids: string[], alertsClient: AlertsClient) => - Promise.all( - ids.map(async (id) => - alertsClient.get({ - id, - }) - ) - ) - .then((rules) => rules.filter((rule) => rule.executionStatus.status === 'error')) - .then((rules) => - rules.reduce((acc, failingRule) => { +export type GetFailingRulesResult = Record; + +export const getFailingRules = async ( + ids: string[], + alertsClient: AlertsClient +): Promise => { + try { + const errorRules = await Promise.all( + ids.map(async (id) => + alertsClient.get({ + id, + }) + ) + ); + return errorRules + .filter((rule) => rule.executionStatus.status === 'error') + .reduce((acc, failingRule) => { const accum = acc; const theRule = failingRule; return { @@ -335,5 +349,8 @@ export const getFailingRules = (ids: string[], alertsClient: AlertsClient) => }, ...accum, }; - }, {} as Record) - ); + }, {}); + } catch (exc) { + throw new Error(`Failed to get executionStatus with AlertsClient: ${exc.message}`); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 8af622e6a128b..fb4763a982f43 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -105,7 +105,7 @@ export interface RuleAlertType extends Alert { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface IRuleStatusAttributes extends Record { +export interface IRuleStatusSOAttributes extends Record { alertId: string; // created alert id. statusDate: StatusDate; lastFailureAt: LastFailureAt | null | undefined; @@ -119,21 +119,35 @@ export interface IRuleStatusAttributes extends Record { searchAfterTimeDurations: string[] | null | undefined; } +export interface IRuleStatusResponseAttributes { + alert_id: string; // created alert id. + status_date: StatusDate; + last_failure_at: LastFailureAt | null | undefined; + last_failure_message: LastFailureMessage | null | undefined; + last_success_at: LastSuccessAt | null | undefined; + last_success_message: LastSuccessMessage | null | undefined; + status: JobStatus | null | undefined; + last_look_back_date: string | null | undefined; + gap: string | null | undefined; + bulk_create_time_durations: string[] | null | undefined; + search_after_time_durations: string[] | null | undefined; +} + export interface RuleStatusResponse { [key: string]: { - current_status: IRuleStatusAttributes | null | undefined; - failures: IRuleStatusAttributes[] | null | undefined; + current_status: IRuleStatusResponseAttributes | null | undefined; + failures: IRuleStatusResponseAttributes[] | null | undefined; }; } export interface IRuleSavedAttributesSavedObjectAttributes - extends IRuleStatusAttributes, + extends IRuleStatusSOAttributes, SavedObjectAttributes {} export interface IRuleStatusSavedObject { type: string; id: string; - attributes: Array>; + attributes: Array>; references: unknown[]; updated_at: string; version: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index cbf70f3119b31..4559a658c9583 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -19,7 +19,7 @@ import { } from '../../../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; import { RuleTypeParams } from '../../types'; -import { IRuleStatusAttributes } from '../../rules/types'; +import { IRuleStatusSOAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; @@ -555,7 +555,7 @@ export const sampleDocSearchResultsWithSortId = ( export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; -export const exampleRuleStatus: () => SavedObject = () => ({ +export const exampleRuleStatus: () => SavedObject = () => ({ type: ruleStatusSavedObjectType, id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e', attributes: { @@ -577,8 +577,10 @@ export const exampleRuleStatus: () => SavedObject = () => }); export const exampleFindRuleStatusResponse: ( - mockStatuses: Array> -) => SavedObjectsFindResponse = (mockStatuses = [exampleRuleStatus()]) => ({ + mockStatuses: Array> +) => SavedObjectsFindResponse = ( + mockStatuses = [exampleRuleStatus()] +) => ({ total: 1, per_page: 6, page: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts index 913efbe04aa16..1ddec9cd15148 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts @@ -6,7 +6,7 @@ import { SavedObject } from 'src/core/server'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; @@ -18,7 +18,7 @@ interface RuleStatusParams { export const createNewRuleStatus = async ({ alertId, ruleStatusClient, -}: RuleStatusParams): Promise> => { +}: RuleStatusParams): Promise> => { const now = new Date().toISOString(); return ruleStatusClient.create({ alertId, @@ -38,7 +38,7 @@ export const createNewRuleStatus = async ({ export const getOrCreateRuleStatuses = async ({ alertId, ruleStatusClient, -}: RuleStatusParams): Promise>> => { +}: RuleStatusParams): Promise>> => { const ruleStatuses = await getRuleStatusSavedObjects({ alertId, ruleStatusClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts index 828b4ea41096e..72a271fb2606f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { MAX_RULE_STATUSES } from './rule_status_service'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; @@ -17,7 +17,7 @@ interface GetRuleStatusSavedObject { export const getRuleStatusSavedObjects = async ({ alertId, ruleStatusClient, -}: GetRuleStatusSavedObject): Promise> => { +}: GetRuleStatusSavedObject): Promise> => { return ruleStatusClient.find({ perPage: MAX_RULE_STATUSES, sortField: 'statusDate', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts index 4b5faeb5b9d27..f6a08852ac8d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts @@ -12,17 +12,17 @@ import { SavedObjectsFindResponse, } from '../../../../../../../src/core/server'; import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; export interface RuleStatusSavedObjectsClient { find: ( options?: Omit - ) => Promise>; - create: (attributes: IRuleStatusAttributes) => Promise>; + ) => Promise>; + create: (attributes: IRuleStatusSOAttributes) => Promise>; update: ( id: string, - attributes: Partial - ) => Promise>; + attributes: Partial + ) => Promise>; delete: (id: string) => Promise<{}>; } @@ -30,7 +30,10 @@ export const ruleStatusSavedObjectsClientFactory = ( savedObjectsClient: SavedObjectsClientContract ): RuleStatusSavedObjectsClient => ({ find: (options) => - savedObjectsClient.find({ ...options, type: ruleStatusSavedObjectType }), + savedObjectsClient.find({ + ...options, + type: ruleStatusSavedObjectType, + }), create: (attributes) => savedObjectsClient.create(ruleStatusSavedObjectType, attributes), update: (id, attributes) => savedObjectsClient.update(ruleStatusSavedObjectType, id, attributes), delete: (id) => savedObjectsClient.delete(ruleStatusSavedObjectType, id), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index 8fdbe282eece5..433ad4e2affea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -6,7 +6,7 @@ import { assertUnreachable } from '../../../../common/utility_types'; import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas'; -import { IRuleStatusAttributes } from '../rules/types'; +import { IRuleStatusSOAttributes } from '../rules/types'; import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; @@ -30,9 +30,9 @@ export const buildRuleStatusAttributes: ( status: JobStatus, message?: string, attributes?: Attributes -) => Partial = (status, message, attributes = {}) => { +) => Partial = (status, message, attributes = {}) => { const now = new Date().toISOString(); - const baseAttributes: Partial = { + const baseAttributes: Partial = { ...attributes, status, statusDate: now, From 3af07940eeba4035d10aadbcb5d1d65d0c883991 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 22 Oct 2020 18:45:44 -0700 Subject: [PATCH 10/15] [Reporting/Tech Debt] Convert PdfMaker class to TypeScript (#81242) (#81400) * convert pdf.js to TS * more typescript * simplify caller * more typescript * more typescript * fix the code to match the expected interface * very cool comment * interface correction * remove unused class method * add unit test for PdfMaker * file rename for typo correction Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 5 +- .../printable_pdf/lib/generate_pdf.ts | 5 +- .../printable_pdf/lib/get_custom_logo.test.ts | 2 +- .../printable_pdf/lib/pdf/get_doc_options.ts | 34 ++ .../printable_pdf/lib/pdf/get_font.ts | 22 ++ .../printable_pdf/lib/pdf/get_template.ts | 130 +++++++ .../printable_pdf/lib/pdf/index.js | 319 ------------------ .../printable_pdf/lib/pdf/index.test.ts | 76 +++++ .../printable_pdf/lib/pdf/index.ts | 148 ++++++++ .../server/lib/layouts/create_layout.ts | 7 +- .../reporting/server/lib/layouts/layout.ts | 8 +- .../server/lib/layouts/preserve_layout.ts | 5 +- .../server/lib/layouts/print_layout.ts | 5 +- yarn.lock | 15 + 14 files changed, 448 insertions(+), 333 deletions(-) create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts diff --git a/package.json b/package.json index e27876c14dd07..014d41c4100a5 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "@elastic/safer-lodash-set": "0.0.0", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", + "@kbn/ace": "1.0.0", "@kbn/analytics": "1.0.0", "@kbn/apm-config-loader": "1.0.0", "@kbn/config": "1.0.0", @@ -128,11 +129,11 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/logging": "1.0.0", + "@kbn/monaco": "1.0.0", "@kbn/std": "1.0.0", "@kbn/ui-framework": "1.0.0", - "@kbn/ace": "1.0.0", - "@kbn/monaco": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/pdfmake": "^0.1.15", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 2cf5b69835d1f..39f597acec6ec 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -12,8 +12,7 @@ import { LevelLogger } from '../../../lib'; import { createLayout, LayoutParams } from '../../../lib/layouts'; import { ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; -// @ts-ignore untyped module -import { pdf } from './pdf'; +import { PdfMaker } from './pdf'; import { getTracker } from './tracker'; const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { @@ -58,7 +57,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.endScreenshots(); tracker.startSetup(); - const pdfOutput = pdf.create(layout, logo); + const pdfOutput = new PdfMaker(layout, logo); if (title) { const timeRange = getTimeRange(results); title += timeRange ? ` - ${timeRange}` : ''; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts index 9f7e9310333ba..fe687c3f47327 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts @@ -31,7 +31,7 @@ test(`gets logo from uiSettings`, async () => { }; const mockGet = jest.fn(); - mockGet.mockImplementationOnce((...args: any[]) => { + mockGet.mockImplementationOnce((...args: string[]) => { if (args[0] === 'xpackReporting:customPdfLogo') { return 'purple pony'; } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts new file mode 100644 index 0000000000000..5a3671835ce51 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BufferOptions } from 'pdfmake/interfaces'; + +export function getDocOptions(tableBorderWidth: number): BufferOptions { + return { + tableLayouts: { + noBorder: { + // format is function (i, node) { ... }; + hLineWidth: () => 0, + vLineWidth: () => 0, + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + simpleBorder: { + // format is function (i, node) { ... }; + hLineWidth: () => tableBorderWidth, + vLineWidth: () => tableBorderWidth, + hLineColor: () => 'silver', + vLineColor: () => 'silver', + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts new file mode 100644 index 0000000000000..5cd6136153f04 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore: no module definition +import xRegExp from 'xregexp'; + +export function getFont(text: string) { + // Once unicode regex scripts are fully supported we should be able to get rid of the dependency + // on xRegExp library. See https://github.com/tc39/proposal-regexp-unicode-property-escapes + // for more information. We are matching Han characters which is one of the supported unicode scripts + // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). + // This will match Chinese, Japanese, Korean and some other Asian languages. + const isCKJ = xRegExp('\\p{Han}').test(text, 'g'); + if (isCKJ) { + return 'noto-cjk'; + } else { + return 'Roboto'; + } +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts new file mode 100644 index 0000000000000..131d289576384 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import path from 'path'; +import { TDocumentDefinitions } from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { getFont } from './get_font'; + +export function getTemplate( + layout: LayoutInstance, + logo: string | undefined, + title: string, + tableBorderWidth: number, + assetPath: string +): Partial { + const pageMarginTop = 40; + const pageMarginBottom = 80; + const pageMarginWidth = 40; + const headingFontSize = 14; + const headingMarginTop = 10; + const headingMarginBottom = 5; + const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; + const subheadingFontSize = 12; + const subheadingMarginTop = 0; + const subheadingMarginBottom = 5; + const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; + + return { + // define page size + pageOrientation: layout.getPdfPageOrientation(), + pageSize: layout.getPdfPageSize({ + pageMarginTop, + pageMarginBottom, + pageMarginWidth, + tableBorderWidth, + headingHeight, + subheadingHeight, + }), + pageMargins: [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom], + + header() { + return { + margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0], + text: title, + font: getFont(title), + style: { + color: '#aaa', + }, + fontSize: 10, + alignment: 'center', + }; + }, + + footer(currentPage: number, pageCount: number) { + const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); // Default Elastic Logo + return { + margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0], + layout: 'noBorder', + table: { + widths: [100, '*', 100], + body: [ + [ + { + fit: [100, 35], + image: logo || logoPath, + }, + { + alignment: 'center', + text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', { + defaultMessage: 'Page {currentPage} of {pageCount}', + values: { currentPage: currentPage.toString(), pageCount }, + }), + style: { + color: '#aaa', + }, + }, + '', + ], + [ + logo + ? { + text: i18n.translate( + 'xpack.reporting.exportTypes.printablePdf.logoDescription', + { + defaultMessage: 'Powered by Elastic', + } + ), + fontSize: 10, + style: { + color: '#aaa', + }, + margin: [0, 2, 0, 0], + } + : '', + '', + '', + ], + ], + }, + }; + }, + + styles: { + heading: { + alignment: 'left', + fontSize: headingFontSize, + bold: true, + margin: [headingMarginTop, 0, headingMarginBottom, 0], + }, + subheading: { + alignment: 'left', + fontSize: subheadingFontSize, + italics: true, + margin: [0, 0, subheadingMarginBottom, 20], + }, + warning: { + color: '#f39c12', // same as @brand-warning in Kibana colors.less + }, + }, + + defaultStyle: { + fontSize: 12, + font: 'Roboto', + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js deleted file mode 100644 index 8840fd524f3e4..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import path from 'path'; -import _ from 'lodash'; -import concat from 'concat-stream'; -import Printer from 'pdfmake'; -import xRegExp from 'xregexp'; -import { i18n } from '@kbn/i18n'; - -const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); - -const tableBorderWidth = 1; - -function getFont(text) { - // Once unicode regex scripts are fully supported we should be able to get rid of the dependency - // on xRegExp library. See https://github.com/tc39/proposal-regexp-unicode-property-escapes - // for more information. We are matching Han characters which is one of the supported unicode scripts - // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). - // This will match Chinese, Japanese, Korean and some other Asian languages. - const isCKJ = xRegExp('\\p{Han}').test(text, 'g'); - if (isCKJ) { - return 'noto-cjk'; - } else { - return 'Roboto'; - } -} - -class PdfMaker { - constructor(layout, logo) { - const fontPath = (filename) => path.resolve(assetPath, 'fonts', filename); - const fonts = { - Roboto: { - normal: fontPath('roboto/Roboto-Regular.ttf'), - bold: fontPath('roboto/Roboto-Medium.ttf'), - italics: fontPath('roboto/Roboto-Italic.ttf'), - bolditalics: fontPath('roboto/Roboto-Italic.ttf'), - }, - 'noto-cjk': { - // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. - normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - }, - }; - - this._layout = layout; - this._logo = logo; - this._title = ''; - this._content = []; - this._printer = new Printer(fonts); - } - - _addContents(contents) { - const groupCount = this._content.length; - - // inject a page break for every 2 groups on the page - if (groupCount > 0 && groupCount % this._layout.groupCount === 0) { - contents = [ - { - text: '', - pageBreak: 'after', - }, - ].concat(contents); - } - this._content.push(contents); - } - - addImage(base64EncodedData, { title = '', description = '' }) { - const contents = []; - - if (title && title.length > 0) { - contents.push({ - text: title, - style: 'heading', - font: getFont(title), - noWrap: true, - }); - } - - if (description && description.length > 0) { - contents.push({ - text: description, - style: 'subheading', - font: getFont(description), - noWrap: true, - }); - } - - const img = { - image: `data:image/png;base64,${base64EncodedData}`, - alignment: 'center', - }; - - const size = this._layout.getPdfImageSize(); - img.height = size.height; - img.width = size.width; - - const wrappedImg = { - table: { - body: [[img]], - }, - layout: 'noBorder', - }; - - contents.push(wrappedImg); - - this._addContents(contents); - } - - addHeading(headingText, opts = {}) { - const contents = []; - contents.push({ - text: headingText, - style: ['heading'].concat(opts.styles || []), - font: getFont(headingText), - }); - this._addContents(contents); - } - - setTitle(title) { - this._title = title; - } - - generate() { - const docTemplate = _.assign(getTemplate(this._layout, this._logo, this._title), { - content: this._content, - }); - this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions()); - return this; - } - - getBuffer() { - if (!this._pdfDoc) { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', - { - defaultMessage: 'Document stream has not been generated', - } - ) - ); - } - return new Promise((resolve, reject) => { - const concatStream = concat(function (pdfBuffer) { - resolve(pdfBuffer); - }); - - this._pdfDoc.on('error', reject); - this._pdfDoc.pipe(concatStream); - this._pdfDoc.end(); - }); - } - - getStream() { - if (!this._pdfDoc) { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', - { - defaultMessage: 'Document stream has not been generated', - } - ) - ); - } - this._pdfDoc.end(); - return this._pdfDoc; - } -} - -function getTemplate(layout, logo, title) { - const pageMarginTop = 40; - const pageMarginBottom = 80; - const pageMarginWidth = 40; - const headingFontSize = 14; - const headingMarginTop = 10; - const headingMarginBottom = 5; - const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; - const subheadingFontSize = 12; - const subheadingMarginTop = 0; - const subheadingMarginBottom = 5; - const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; - - return { - // define page size - pageOrientation: layout.getPdfPageOrientation(), - pageSize: layout.getPdfPageSize({ - pageMarginTop, - pageMarginBottom, - pageMarginWidth, - tableBorderWidth, - headingHeight, - subheadingHeight, - }), - pageMargins: [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom], - - header: function () { - return { - margin: [pageMarginWidth, pageMarginTop / 4, pageMarginWidth, 0], - text: title, - font: getFont(title), - style: { - color: '#aaa', - }, - fontSize: 10, - alignment: 'center', - }; - }, - - footer: function (currentPage, pageCount) { - const logoPath = path.resolve(assetPath, 'img', 'logo-grey.png'); - return { - margin: [pageMarginWidth, pageMarginBottom / 4, pageMarginWidth, 0], - layout: 'noBorder', - table: { - widths: [100, '*', 100], - body: [ - [ - { - fit: [100, 35], - image: logo || logoPath, - }, - { - alignment: 'center', - text: i18n.translate('xpack.reporting.exportTypes.printablePdf.pagingDescription', { - defaultMessage: 'Page {currentPage} of {pageCount}', - values: { currentPage: currentPage.toString(), pageCount }, - }), - style: { - color: '#aaa', - }, - }, - '', - ], - [ - logo - ? { - text: i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.logoDescription', - { - defaultMessage: 'Powered by Elastic', - } - ), - fontSize: 10, - style: { - color: '#aaa', - }, - margin: [0, 2, 0, 0], - } - : '', - '', - '', - ], - ], - }, - }; - }, - - styles: { - heading: { - alignment: 'left', - fontSize: headingFontSize, - bold: true, - marginTop: headingMarginTop, - marginBottom: headingMarginBottom, - }, - subheading: { - alignment: 'left', - fontSize: subheadingFontSize, - italics: true, - marginLeft: 20, - marginBottom: subheadingMarginBottom, - }, - warning: { - color: '#f39c12', // same as @brand-warning in Kibana colors.less - }, - }, - - defaultStyle: { - fontSize: 12, - font: 'Roboto', - }, - }; -} - -function getDocOptions() { - return { - tableLayouts: { - noBorder: { - // format is function (i, node) { ... }; - hLineWidth: () => 0, - vLineWidth: () => 0, - paddingLeft: () => 0, - paddingRight: () => 0, - paddingTop: () => 0, - paddingBottom: () => 0, - }, - simpleBorder: { - // format is function (i, node) { ... }; - hLineWidth: () => tableBorderWidth, - vLineWidth: () => tableBorderWidth, - hLineColor: () => 'silver', - vLineColor: () => 'silver', - paddingLeft: () => 0, - paddingRight: () => 0, - paddingTop: () => 0, - paddingBottom: () => 0, - }, - }, - }; -} - -export const pdf = { - create: (layout, logo) => new PdfMaker(layout, logo), -}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts new file mode 100644 index 0000000000000..5c6a7c7c63c69 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PreserveLayout, PrintLayout } from '../../../../lib/layouts'; +import { createMockConfig, createMockConfigSchema } from '../../../../test_helpers'; +import { PdfMaker } from './'; + +const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`; + +describe('PdfMaker', () => { + it('makes PDF using PrintLayout mode', async () => { + const config = createMockConfig(createMockConfigSchema()); + const layout = new PrintLayout(config.get('capture')); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the best PDF in the world')).toBe(undefined); + expect([ + pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }), + pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }), + ]).toEqual([undefined, undefined]); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + captureConfig: { browser: { chromium: { disableSandbox: true } } }, // NOTE: irrelevant data? + groupCount: 2, + id: 'print', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-item]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the best PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); + + it('makes PDF using PreserveLayout mode', async () => { + const layout = new PreserveLayout({ width: 400, height: 300 }); + const pdf = new PdfMaker(layout, undefined); + + expect(pdf.setTitle('the finest PDF in the world')).toBe(undefined); + expect(pdf.addImage(imageBase64, { title: 'cool times', description: '☃️' })).toBe(undefined); + + const { _layout: testLayout, _title: testTitle } = (pdf as unknown) as { + _layout: object; + _title: string; + }; + expect(testLayout).toMatchObject({ + groupCount: 1, + id: 'preserve_layout', + selectors: { + itemsCountAttribute: 'data-shared-items-count', + renderComplete: '[data-shared-item]', + screenshot: '[data-shared-items-container]', + timefilterDurationAttribute: 'data-shared-timefilter-duration', + }, + }); + expect(testTitle).toBe('the finest PDF in the world'); + + // generate buffer + pdf.generate(); + const result = await pdf.getBuffer(); + expect(Buffer.isBuffer(result)).toBe(true); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts new file mode 100644 index 0000000000000..c58ceae85657b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +// @ts-ignore: no module definition +import concat from 'concat-stream'; +import _ from 'lodash'; +import path from 'path'; +import Printer from 'pdfmake'; +import { Content, ContentText } from 'pdfmake/interfaces'; +import { LayoutInstance } from '../../../../lib/layouts'; +import { getDocOptions } from './get_doc_options'; +import { getFont } from './get_font'; +import { getTemplate } from './get_template'; + +const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); +const tableBorderWidth = 1; + +export class PdfMaker { + private _layout: LayoutInstance; + private _logo: string | undefined; + private _title: string; + private _content: Content[]; + private _printer: Printer; + private _pdfDoc: PDFKit.PDFDocument | undefined; + + constructor(layout: LayoutInstance, logo: string | undefined) { + const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename); + const fonts = { + Roboto: { + normal: fontPath('roboto/Roboto-Regular.ttf'), + bold: fontPath('roboto/Roboto-Medium.ttf'), + italics: fontPath('roboto/Roboto-Italic.ttf'), + bolditalics: fontPath('roboto/Roboto-Italic.ttf'), + }, + 'noto-cjk': { + // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. + normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + }, + }; + + this._layout = layout; + this._logo = logo; + this._title = ''; + this._content = []; + this._printer = new Printer(fonts); + } + + _addContents(contents: Content[]) { + const groupCount = this._content.length; + + // inject a page break for every 2 groups on the page + if (groupCount > 0 && groupCount % this._layout.groupCount === 0) { + contents = [ + ({ + text: '', + pageBreak: 'after', + } as ContentText) as Content, + ].concat(contents); + } + this._content.push(contents); + } + + addImage(base64EncodedData: string, { title = '', description = '' }) { + const contents: Content[] = []; + + if (title && title.length > 0) { + contents.push({ + text: title, + style: 'heading', + font: getFont(title), + noWrap: true, + }); + } + + if (description && description.length > 0) { + contents.push({ + text: description, + style: 'subheading', + font: getFont(description), + noWrap: true, + }); + } + + const size = this._layout.getPdfImageSize(); + const img = { + image: `data:image/png;base64,${base64EncodedData}`, + alignment: 'center', + height: size.height, + width: size.width, + }; + + const wrappedImg = { + table: { + body: [[img]], + }, + layout: 'noBorder', + }; + + contents.push(wrappedImg); + + this._addContents(contents); + } + + setTitle(title: string) { + this._title = title; + } + + generate() { + const docTemplate = _.assign( + getTemplate(this._layout, this._logo, this._title, tableBorderWidth, assetPath), + { + content: this._content, + } + ); + this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions(tableBorderWidth)); + return this; + } + + getBuffer(): Promise { + return new Promise((resolve, reject) => { + if (!this._pdfDoc) { + throw new Error( + i18n.translate( + 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', + { + defaultMessage: 'Document stream has not been generated', + } + ) + ); + } + + const concatStream = concat(function (pdfBuffer: Buffer) { + resolve(pdfBuffer); + }); + + this._pdfDoc.on('error', reject); + this._pdfDoc.pipe(concatStream); + this._pdfDoc.end(); + }); + } +} diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index 585175aac82c5..e69b8d61dec0d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -5,11 +5,14 @@ */ import { CaptureConfig } from '../../types'; -import { LayoutParams, LayoutTypes } from './'; +import { LayoutInstance, LayoutParams, LayoutTypes } from './'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; -export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams) { +export function createLayout( + captureConfig: CaptureConfig, + layoutParams?: LayoutParams +): LayoutInstance { if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/layout.ts b/x-pack/plugins/reporting/server/lib/layouts/layout.ts index 433edb35df4cd..4dd4003c269c0 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/layout.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces'; import { PageSizeParams, PdfImageSize, Size } from './'; export interface ViewZoomWidthHeight { @@ -14,6 +15,7 @@ export interface ViewZoomWidthHeight { export abstract class Layout { public id: string = ''; + public groupCount: number = 0; constructor(id: string) { this.id = id; @@ -21,9 +23,11 @@ export abstract class Layout { public abstract getPdfImageSize(): PdfImageSize; - public abstract getPdfPageOrientation(): string | undefined; + public abstract getPdfPageOrientation(): 'portrait' | 'landscape' | undefined; - public abstract getPdfPageSize(pageSizeParams: PageSizeParams): string | Size; + public abstract getPdfPageSize( + pageSizeParams: PageSizeParams + ): CustomPageSize | PredefinedPageSize; public abstract getViewport(itemsCount: number): ViewZoomWidthHeight | null; diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index cecd761fbcf32..faddaae64ce5d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -5,14 +5,15 @@ */ import path from 'path'; +import { CustomPageSize } from 'pdfmake/interfaces'; import { getDefaultLayoutSelectors, Layout, + LayoutInstance, LayoutSelectorDictionary, LayoutTypes, PageSizeParams, Size, - LayoutInstance, } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. @@ -72,7 +73,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { return undefined; } - public getPdfPageSize(pageSizeParams: PageSizeParams) { + public getPdfPageSize(pageSizeParams: PageSizeParams): CustomPageSize { return { height: this.height + diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 33f16bc7865d5..e979cdeeb71fe 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -5,6 +5,7 @@ */ import path from 'path'; +import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; @@ -90,11 +91,11 @@ export class PrintLayout extends Layout implements LayoutInstance { }; } - public getPdfPageOrientation() { + public getPdfPageOrientation(): PageOrientation { return 'portrait'; } - public getPdfPageSize() { + public getPdfPageSize(): PredefinedPageSize { return 'A4'; } } diff --git a/yarn.lock b/yarn.lock index 424ae8f91d1d0..0fbc8c05a140d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4588,6 +4588,21 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== +"@types/pdfkit@*": + version "0.10.6" + resolved "https://registry.yarnpkg.com/@types/pdfkit/-/pdfkit-0.10.6.tgz#9ddde7e642e6c3f1245134456a03fbc4b67dc4b5" + integrity sha512-o6R2fO/fhg392YNYahiaNGZ8CNdSKMmf5W0LvyjJ/63mLQEPTgl6M9vb4zKoHaGpsP43VimuBz+xgVevsPy8jA== + dependencies: + "@types/node" "*" + +"@types/pdfmake@^0.1.15": + version "0.1.15" + resolved "https://registry.yarnpkg.com/@types/pdfmake/-/pdfmake-0.1.15.tgz#383a8fca407612a580b82d1ca496d39001aee102" + integrity sha512-uyKefZzC1OUTKoUdY0fU9n7BjciSSYPHq2KLQmGNejeZn6Xo6hI04xhAGk368Rv9wHjKo36IGLIVIysvhrGVJQ== + dependencies: + "@types/node" "*" + "@types/pdfkit" "*" + "@types/pegjs@^0.10.1": version "0.10.1" resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94" From b0f79153865d8fd0a27733a71cae60dc01d8dceb Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 23 Oct 2020 13:50:57 -0400 Subject: [PATCH 11/15] [DOC] Update docs for new monitoring alerts (#80922) (#81364) * Update docs for new alerts * PR feedback * Fix bad copy/pate --- docs/user/monitoring/kibana-alerts.asciidoc | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 1ac5c385f8ed5..300497126c3e5 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -31,6 +31,33 @@ default, the trigger condition is set at 85% or more averaged over the last 5 minutes. The alert is grouped across all the nodes of the cluster by running checks on a schedule time of 1 minute with a re-notify internal of 1 day. +[discrete] +[[kibana-alerts-disk-usage-threshold]] +== Disk usage threshold + +This alert is triggered when a node is nearly at disk capacity. By +default, the trigger condition is set at 80% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-jvm-memory-threshold]] +== JVM memory threshold + +This alert is triggered when a node runs a consistently high JVM memory usage. By +default, the trigger condition is set at 85% or more averaged over the last 5 +minutes. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 1 day. + +[discrete] +[[kibana-alerts-missing-monitoring-data]] +== Missing monitoring data + +This alert is triggered when any stack products nodes or instances stop sending +monitoring data. By default, the trigger condition is set to missing for 15 minutes +looking back 1 day. The alert is grouped across all the nodes of the cluster by running +checks on a schedule time of 1 minute with a re-notify internal of 6 hours. + NOTE: Some action types are subscription features, while others are free. For a comparison of the Elastic subscription levels, see the alerting section of the {subscriptions}[Subscriptions page]. From 77081a1cb42e74666102d356b6d845fe14d18ae1 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 23 Oct 2020 11:08:18 -0700 Subject: [PATCH 12/15] Allow default packages to be deleted from the default agent policy (#81535) (#81542) --- .../server/services/agent_policy.ts | 23 ++++-- .../ingest_manager/server/services/setup.ts | 72 ++++++++++--------- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index 19d69a33788c6..b003d16d379ca 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -83,7 +83,12 @@ class AgentPolicyService { return (await this.get(soClient, id)) as AgentPolicy; } - public async ensureDefaultAgentPolicy(soClient: SavedObjectsClientContract) { + public async ensureDefaultAgentPolicy( + soClient: SavedObjectsClientContract + ): Promise<{ + created: boolean; + defaultAgentPolicy: AgentPolicy; + }> { const agentPolicies = await soClient.find({ type: AGENT_POLICY_SAVED_OBJECT_TYPE, searchFields: ['is_default'], @@ -95,12 +100,18 @@ class AgentPolicyService { ...DEFAULT_AGENT_POLICY, }; - return this.create(soClient, newDefaultAgentPolicy); + return { + created: true, + defaultAgentPolicy: await this.create(soClient, newDefaultAgentPolicy), + }; } return { - id: agentPolicies.saved_objects[0].id, - ...agentPolicies.saved_objects[0].attributes, + created: false, + defaultAgentPolicy: { + id: agentPolicies.saved_objects[0].id, + ...agentPolicies.saved_objects[0].attributes, + }, }; } @@ -404,7 +415,9 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } - const { id: defaultAgentPolicyId } = await this.ensureDefaultAgentPolicy(soClient); + const { + defaultAgentPolicy: { id: defaultAgentPolicyId }, + } = await this.ensureDefaultAgentPolicy(soClient); if (id === defaultAgentPolicyId) { throw new Error('The default agent policy cannot be deleted'); } diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 7f379d3ea4f13..741a23824f010 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -49,7 +49,11 @@ async function createSetupSideEffects( soClient: SavedObjectsClientContract, callCluster: CallESAsCurrentUser ): Promise { - const [installedPackages, defaultOutput, defaultAgentPolicy] = await Promise.all([ + const [ + installedPackages, + defaultOutput, + { created: defaultAgentPolicyCreated, defaultAgentPolicy }, + ] = await Promise.all([ // packages installed by default ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), @@ -66,44 +70,46 @@ async function createSetupSideEffects( }), ]); - // ensure default packages are added to the default conifg - const agentPolicyWithPackagePolicies = await agentPolicyService.get( - soClient, - defaultAgentPolicy.id, - true - ); - if (!agentPolicyWithPackagePolicies) { - throw new Error('Policy not found'); - } - if ( - agentPolicyWithPackagePolicies.package_policies.length && - typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string' - ) { - throw new Error('Policy not found'); - } - - for (const installedPackage of installedPackages) { - const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some( - (packageName) => installedPackage.name === packageName + // If we just created the default policy, ensure default packages are added to it + if (defaultAgentPolicyCreated) { + const agentPolicyWithPackagePolicies = await agentPolicyService.get( + soClient, + defaultAgentPolicy.id, + true ); - if (!packageShouldBeInstalled) { - continue; + if (!agentPolicyWithPackagePolicies) { + throw new Error('Policy not found'); + } + if ( + agentPolicyWithPackagePolicies.package_policies.length && + typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string' + ) { + throw new Error('Policy not found'); } - const isInstalled = agentPolicyWithPackagePolicies.package_policies.some( - (d: PackagePolicy | string) => { - return typeof d !== 'string' && d.package?.name === installedPackage.name; + for (const installedPackage of installedPackages) { + const packageShouldBeInstalled = DEFAULT_AGENT_POLICIES_PACKAGES.some( + (packageName) => installedPackage.name === packageName + ); + if (!packageShouldBeInstalled) { + continue; } - ); - if (!isInstalled) { - await addPackageToAgentPolicy( - soClient, - callCluster, - installedPackage, - agentPolicyWithPackagePolicies, - defaultOutput + const isInstalled = agentPolicyWithPackagePolicies.package_policies.some( + (d: PackagePolicy | string) => { + return typeof d !== 'string' && d.package?.name === installedPackage.name; + } ); + + if (!isInstalled) { + await addPackageToAgentPolicy( + soClient, + callCluster, + installedPackage, + agentPolicyWithPackagePolicies, + defaultOutput + ); + } } } From 252dd625f1390d5c56cbe585c967e9a11a71a578 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Fri, 23 Oct 2020 22:01:58 +0100 Subject: [PATCH 13/15] update date picker component (#79418) (#81494) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__tests__/uptime_date_picker.test.tsx | 66 ++++++++++++++++++- .../components/common/uptime_date_picker.tsx | 53 +++++++++++++-- .../lib/__mocks__/uptime_plugin_start_mock.ts | 23 +++++++ .../public/lib/helper/helper_with_router.tsx | 15 ++--- 4 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts diff --git a/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx index 16853211433ca..18e058e305606 100644 --- a/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx @@ -6,7 +6,16 @@ import React from 'react'; import { UptimeDatePicker } from '../uptime_date_picker'; -import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib'; +import { + renderWithRouter, + shallowWithRouter, + MountWithReduxProvider, + mountWithRouterRedux, +} from '../../../lib'; +import { UptimeStartupPluginsContextProvider } from '../../../contexts'; +import { startPlugins } from '../../../lib/__mocks__/uptime_plugin_start_mock'; +import { ClientPluginsStart } from '../../../apps/plugin'; +import { createMemoryHistory } from 'history'; describe('UptimeDatePicker component', () => { it('validates props with shallow render', () => { @@ -22,4 +31,59 @@ describe('UptimeDatePicker component', () => { ); expect(component).toMatchSnapshot(); }); + + it('uses shared date range state when there is no url date range state', () => { + const customHistory = createMemoryHistory(); + jest.spyOn(customHistory, 'push'); + + const component = mountWithRouterRedux( + )} + > + + , + { customHistory } + ); + + const startBtn = component.find('[data-test-subj="superDatePickerstartDatePopoverButton"]'); + + expect(startBtn.text()).toBe('~ 30 minutes ago'); + + const endBtn = component.find('[data-test-subj="superDatePickerendDatePopoverButton"]'); + + expect(endBtn.text()).toBe('~ 15 minutes ago'); + + expect(customHistory.push).toHaveBeenCalledWith({ + pathname: '/', + search: 'dateRangeStart=now-30m&dateRangeEnd=now-15m', + }); + }); + + it('should use url date range even if shared date range is present', () => { + const customHistory = createMemoryHistory({ + initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], + }); + + jest.spyOn(customHistory, 'push'); + + const component = mountWithRouterRedux( + )} + > + + , + { customHistory } + ); + + const showDateBtn = component.find('[data-test-subj="superDatePickerShowDatesButton"]'); + + expect(showDateBtn.childAt(0).text()).toBe('Last 10 minutes'); + + // it should update shared state + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({ + from: 'now-10m', + to: 'now', + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx index 1d0dcad73795b..cc8d6271abd73 100644 --- a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx +++ b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx @@ -4,11 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { EuiSuperDatePicker } from '@elastic/eui'; import { useUrlParams } from '../../hooks'; import { CLIENT_DEFAULTS } from '../../../common/constants'; -import { UptimeRefreshContext, UptimeSettingsContext } from '../../contexts'; +import { + UptimeRefreshContext, + UptimeSettingsContext, + UptimeStartupPluginsContext, +} from '../../contexts'; export interface CommonlyUsedRange { from: string; @@ -16,12 +20,43 @@ export interface CommonlyUsedRange { display: string; } +const isUptimeDefaultDateRange = (dateRangeStart: string, dateRangeEnd: string) => { + const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS; + + return dateRangeStart === DATE_RANGE_START && dateRangeEnd === DATE_RANGE_END; +}; + export const UptimeDatePicker = () => { const [getUrlParams, updateUrl] = useUrlParams(); - const { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd } = getUrlParams(); const { commonlyUsedRanges } = useContext(UptimeSettingsContext); const { refreshApp } = useContext(UptimeRefreshContext); + const { data } = useContext(UptimeStartupPluginsContext); + + // read time from state and update the url + const sharedTimeState = data?.query.timefilter.timefilter.getTime(); + + const { + autorefreshInterval, + autorefreshIsPaused, + dateRangeStart: start, + dateRangeEnd: end, + } = getUrlParams(); + + useEffect(() => { + const { from, to } = sharedTimeState ?? {}; + // if it's uptime default range, and we have shared state from kibana, let's use that + if (isUptimeDefaultDateRange(start, end) && (from !== start || to !== end)) { + updateUrl({ dateRangeStart: from, dateRangeEnd: to }); + } else if (from !== start || to !== end) { + // if it's coming url. let's update shared state + data?.query.timefilter.timefilter.setTime({ from: start, to: end }); + } + + // only need at start, rest date picker on change fucn will take care off + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const euiCommonlyUsedRanges = commonlyUsedRanges ? commonlyUsedRanges.map( ({ from, to, display }: { from: string; to: string; display: string }) => { @@ -36,13 +71,17 @@ export const UptimeDatePicker = () => { return ( { - updateUrl({ dateRangeStart: start, dateRangeEnd: end }); + onTimeChange={({ start: startN, end: endN }) => { + if (data?.query?.timefilter?.timefilter) { + data?.query.timefilter.timefilter.setTime({ from: startN, to: endN }); + } + + updateUrl({ dateRangeStart: startN, dateRangeEnd: endN }); refreshApp(); }} onRefresh={refreshApp} diff --git a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts new file mode 100644 index 0000000000000..6d2ea80a3b6f2 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_plugin_start_mock.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface InputTimeRange { + from: string; + to: string; +} + +export const startPlugins = { + data: { + query: { + timefilter: { + timefilter: { + getTime: () => ({ to: 'now-15m', from: 'now-30m' }), + setTime: jest.fn(({ from, to }: InputTimeRange) => {}), + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx index 7da570e909425..5219fb3242539 100644 --- a/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx @@ -20,22 +20,19 @@ const helperWithRouter: ( wrapReduxStore?: boolean, storeState?: AppState ) => R = (helper, component, customHistory, wrapReduxStore, storeState) => { - if (customHistory) { - customHistory.location.key = 'TestKeyForTesting'; - return helper({component}); - } - const history = createMemoryHistory(); + const history = customHistory ?? createMemoryHistory(); + history.location.key = 'TestKeyForTesting'; + const routerWrapper = {component}; + if (wrapReduxStore) { return helper( - - {component} - + {routerWrapper} ); } - return helper({component}); + return helper(routerWrapper); }; export const renderWithRouter = (component: ReactElement, customHistory?: MemoryHistory) => { From 485b4c4791735c9d2dc319b1f8bfcba19c07f465 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 23 Oct 2020 15:51:27 -0700 Subject: [PATCH 14/15] Add exact match filtering to in memory tables (#81539) (#81570) --- .../package_policies_table.tsx | 26 +------- .../sections/data_stream/list_page/index.tsx | 63 +++++++++++-------- 2 files changed, 41 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index fa9ce24935429..af4c2f78f14a2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -22,7 +22,6 @@ import { useCapabilities, useLink } from '../../../../../hooks'; import { useAgentPolicyRefresh } from '../../hooks'; interface InMemoryPackagePolicy extends PackagePolicy { - inputTypes: string[]; packageName?: string; packageTitle?: string; packageVersion?: string; @@ -56,11 +55,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ // With the package policies provided on input, generate the list of package policies // used in the InMemoryTable (flattens some values for search) as well as // the list of options that will be used in the filters dropdowns - const [packagePolicies, namespaces, inputTypes] = useMemo((): [ - InMemoryPackagePolicy[], - FilterOption[], - FilterOption[] - ] => { + const [packagePolicies, namespaces] = useMemo((): [InMemoryPackagePolicy[], FilterOption[]] => { const namespacesValues: string[] = []; const inputTypesValues: string[] = []; const mappedPackagePolicies = originalPackagePolicies.map( @@ -69,13 +64,8 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ namespacesValues.push(packagePolicy.namespace); } - const dsInputTypes: string[] = []; - - dsInputTypes.sort(stringSortAscending); - return { ...packagePolicy, - inputTypes: dsInputTypes, packageName: packagePolicy.package?.name ?? '', packageTitle: packagePolicy.package?.title ?? '', packageVersion: packagePolicy.package?.version ?? '', @@ -86,11 +76,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ namespacesValues.sort(stringSortAscending); inputTypesValues.sort(stringSortAscending); - return [ - mappedPackagePolicies, - namespacesValues.map(toFilterOption), - inputTypesValues.map(toFilterOption), - ]; + return [mappedPackagePolicies, namespacesValues.map(toFilterOption)]; }, [originalPackagePolicies]); const columns = useMemo( @@ -273,13 +259,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ name: 'Namespace', options: namespaces, multiSelect: 'or', - }, - { - type: 'field_value_selection', - field: 'inputTypes', - name: 'Input types', - options: inputTypes, - multiSelect: 'or', + operator: 'exact', }, ], }} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index bb109d766c50a..4e32fa0bbc1b9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -182,7 +182,12 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { [] ); - const filterOptions: { [key: string]: string[] } = { + const filterOptions: { + [key: string]: Array<{ + value: string; + name: string; + }>; + } = { dataset: [], type: [], namespace: [], @@ -190,21 +195,37 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { }; if (dataStreamsData && dataStreamsData.data_streams.length) { + const dataValues: { + [key: string]: string[]; + } = { + dataset: [], + type: [], + namespace: [], + package: [], + }; dataStreamsData.data_streams.forEach((stream) => { const { dataset, type, namespace, package: pkg } = stream; - if (!filterOptions.dataset.includes(dataset)) { - filterOptions.dataset.push(dataset); + if (!dataValues.dataset.includes(dataset)) { + dataValues.dataset.push(dataset); } - if (!filterOptions.type.includes(type)) { - filterOptions.type.push(type); + if (!dataValues.type.includes(type)) { + dataValues.type.push(type); } - if (!filterOptions.namespace.includes(namespace)) { - filterOptions.namespace.push(namespace); + if (!dataValues.namespace.includes(namespace)) { + dataValues.namespace.push(namespace); } - if (!filterOptions.package.includes(pkg)) { - filterOptions.package.push(pkg); + if (!dataValues.package.includes(pkg)) { + dataValues.package.push(pkg); } }); + for (const field in dataValues) { + if (filterOptions[field]) { + filterOptions[field] = dataValues[field].sort().map((option) => ({ + value: option, + name: option, + })); + } + } } return ( @@ -266,10 +287,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Dataset', }), multiSelect: 'or', - options: filterOptions.dataset.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.dataset, }, { type: 'field_value_selection', @@ -278,10 +297,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Type', }), multiSelect: 'or', - options: filterOptions.type.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.type, }, { type: 'field_value_selection', @@ -290,10 +307,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Namespace', }), multiSelect: 'or', - options: filterOptions.namespace.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.namespace, }, { type: 'field_value_selection', @@ -302,10 +317,8 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Integration', }), multiSelect: 'or', - options: filterOptions.package.map((option) => ({ - value: option, - name: option, - })), + operator: 'exact', + options: filterOptions.package, }, ], }} From 27163f7b9949f740d3b11a5dc97608bc5494e81f Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Sun, 25 Oct 2020 16:38:50 +0300 Subject: [PATCH 15/15] [7.x] [Plugin generator] Update readme (#81131) (#81578) --- packages/kbn-plugin-generator/README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/kbn-plugin-generator/README.md b/packages/kbn-plugin-generator/README.md index 9ff9a8aa95ca2..bee8e6c2ca783 100644 --- a/packages/kbn-plugin-generator/README.md +++ b/packages/kbn-plugin-generator/README.md @@ -51,7 +51,7 @@ yarn kbn bootstrap Generated plugins receive a handful of scripts that can be used during development. Those scripts are detailed in the [README.md](template/README.md) file in each newly generated plugin, and expose the scripts provided by the [Kibana plugin helpers](../kbn-plugin-helpers), but here is a quick reference in case you need it: -> ***NOTE:*** All of these scripts should be run from the generated plugin. +> ***NOTE:*** The following scripts should be run from the generated plugin. - `yarn kbn bootstrap` @@ -59,14 +59,6 @@ Generated plugins receive a handful of scripts that can be used during developme > ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change. - - `yarn start` - - Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana` - - ``` - yarn start --elasticsearch.hosts http://localhost:9220 - ``` - - `yarn build` Build a distributable archive of your plugin. @@ -75,4 +67,15 @@ Generated plugins receive a handful of scripts that can be used during developme Run the server tests using mocha. + +To start kibana run the following command from Kibana root. + + - `yarn start` + + Start kibana and it will automatically include this plugin. You can pass any arguments that you would normally send to `bin/kibana` + + ``` + yarn start --elasticsearch.hosts http://localhost:9220 + ``` + For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks run `yarn run` or take a look in the `package.json` file.