From da0da4ca752f01428e126964039b0ac867580c21 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Fri, 21 Aug 2020 09:43:42 -0400 Subject: [PATCH 01/25] [Security Solution] modify circular deps checker to output images of circular deps graphs (#75579) --- .../run_check_circular_deps_cli.js | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js b/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js index 9b4a57f09066d..ac4102184091d 100644 --- a/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; - /* eslint-disable-next-line import/no-extraneous-dependencies */ import madge from 'madge'; /* eslint-disable-next-line import/no-extraneous-dependencies */ import { run, createFailError } from '@kbn/dev-utils'; +import * as os from 'os'; +import * as path from 'path'; run( - async ({ log }) => { + async ({ log, flags }) => { const result = await madge( - [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], + [path.resolve(__dirname, '../../public'), path.resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], excludeRegExp: [ @@ -34,6 +34,13 @@ run( const circularFound = result.circular(); if (circularFound.length !== 0) { + if (flags.svg) { + await outputSVGs(circularFound); + } else { + console.log( + 'Run this program with the --svg flag to save an SVG showing the dependency graph.' + ); + } throw createFailError( `SIEM circular dependencies of imports has been found:\n - ${circularFound.join('\n - ')}` ); @@ -42,6 +49,34 @@ run( } }, { - description: 'Check the SIEM plugin for circular deps', + description: + 'Check the Security Solution plugin for circular deps. If any are found, this will throw an Error.', + flags: { + help: ' --svg, Output SVGs of circular dependency graphs', + boolean: ['svg'], + default: { + svg: false, + }, + }, } ); + +async function outputSVGs(circularFound) { + let count = 0; + for (const found of circularFound) { + // Calculate the path using the os tmpdir and an increasing 'count' + const expectedImagePath = path.join(os.tmpdir(), `security_solution-circular-dep-${count}.svg`); + console.log(`Attempting to save SVG for circular dependency: ${found}`); + count++; + + // Graph just the files in the found circular dependency. + const specificGraph = await madge(found, { + fileExtensions: ['ts', 'js', 'tsx'], + }); + + // Output an SVG in the tmp directory + const imagePath = await specificGraph.image(expectedImagePath); + + console.log(`Saved SVG: ${imagePath}`); + } +} From c68363995b5fc0ce6820c5cdd92ab0d8d6d29d58 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 21 Aug 2020 16:15:12 +0200 Subject: [PATCH 02/25] Improve login UI error message. (#75642) --- .../components/login_form/login_form.test.tsx | 49 +++++++++++++++++++ .../components/login_form/login_form.tsx | 14 ++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx index 552d523fa4a84..b6dd06595ae7f 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx @@ -421,9 +421,58 @@ describe('LoginForm', () => { expect(window.location.href).toBe(currentURL); expect(coreStartMock.notifications.toasts.addError).toHaveBeenCalledWith(failureReason, { title: 'Could not perform login.', + toastMessage: 'Oh no!', }); }); + it('shows error with message in the `body`', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.post.mockRejectedValue({ + body: { message: 'Oh no! But with much more details!' }, + message: 'Oh no!', + }); + + window.location.href = currentURL; + const wrapper = mountWithIntl( + + ); + + expectPageMode(wrapper, PageMode.Selector); + + wrapper.findWhere((node) => node.key() === 'saml1').simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith('/internal/security/login', { + body: JSON.stringify({ providerType: 'saml', providerName: 'saml1', currentURL }), + }); + + expect(window.location.href).toBe(currentURL); + expect(coreStartMock.notifications.toasts.addError).toHaveBeenCalledWith( + new Error('Oh no! But with much more details!'), + { title: 'Could not perform login.', toastMessage: 'Oh no!' } + ); + }); + it('properly switches to login form', async () => { const currentURL = `https://some-host/login?next=${encodeURIComponent( '/some-base-path/app/kibana#/home?_g=()' diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index 9ea553af75e00..a929b50fa1ffa 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -451,11 +451,15 @@ export class LoginForm extends Component { window.location.href = location; } catch (err) { - this.props.notifications.toasts.addError(err, { - title: i18n.translate('xpack.security.loginPage.loginSelectorErrorMessage', { - defaultMessage: 'Could not perform login.', - }), - }); + this.props.notifications.toasts.addError( + err?.body?.message ? new Error(err?.body?.message) : err, + { + title: i18n.translate('xpack.security.loginPage.loginSelectorErrorMessage', { + defaultMessage: 'Could not perform login.', + }), + toastMessage: err?.message, + } + ); this.setState({ loadingState: { type: LoadingStateType.None } }); } From 6b3ce3f91ee53c9050401af1edb80bd8793cf1c4 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 21 Aug 2020 10:18:14 -0400 Subject: [PATCH 03/25] [Dashboard First] Lens Originating App Breadcrumb (#75470) Changed lens breadcrumbs to reflect the Originating App --- .../lens/public/app_plugin/app.test.tsx | 34 +++++++++++++++++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 21 +++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index f92343183a700..70136a486e8c1 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -136,6 +136,7 @@ describe('Lens App', () => { originatingApp: string | undefined; onAppLeave: AppMountParameters['onAppLeave']; history: History; + getAppNameFromId?: (appId: string) => string | undefined; }> { return ({ navigation: navigationStartMock, @@ -187,6 +188,7 @@ describe('Lens App', () => { originatingApp: string | undefined; onAppLeave: AppMountParameters['onAppLeave']; history: History; + getAppNameFromId?: (appId: string) => string | undefined; }>; } @@ -298,6 +300,38 @@ describe('Lens App', () => { ]); }); + it('sets originatingApp breadcrumb when the document title changes', async () => { + const defaultArgs = makeDefaultArgs(); + defaultArgs.originatingApp = 'ultraCoolDashboard'; + defaultArgs.getAppNameFromId = () => 'The Coolest Container Ever Made'; + instance = mount(); + + expect(core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Create' }, + ]); + + (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'Daaaaaaadaumching!', + expression: 'valid expression', + state: { + query: 'fake query', + datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + }, + }); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + expect(defaultArgs.core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Daaaaaaadaumching!' }, + ]); + }); + describe('persistence', () => { it('does not load a document if there is no document id', () => { const args = makeDefaultArgs(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index b20fe2f804683..5ca6f27a0c578 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { AppMountContext, AppMountParameters, NotificationsStart } from 'kibana/public'; import { History } from 'history'; +import { EuiBreadcrumb } from '@elastic/eui'; import { Query, DataPublicPluginStart, @@ -203,6 +204,16 @@ export function App({ // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { core.chrome.setBreadcrumbs([ + ...(originatingApp && getAppNameFromId + ? [ + { + onClick: (e) => { + core.application.navigateToApp(originatingApp); + }, + text: getAppNameFromId(originatingApp), + } as EuiBreadcrumb, + ] + : []), { href: core.http.basePath.prepend(`/app/visualize#/`), onClick: (e) => { @@ -219,7 +230,15 @@ export function App({ : i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create' }), }, ]); - }, [core.application, core.chrome, core.http.basePath, state.persistedDoc]); + }, [ + core.application, + core.chrome, + core.http.basePath, + state.persistedDoc, + originatingApp, + redirectTo, + getAppNameFromId, + ]); useEffect( () => { From 338b61ce6c1643af23b42e9a130085b0a55e8e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 21 Aug 2020 15:23:39 +0100 Subject: [PATCH 04/25] [Usage Collection Schemas] Remove Legacy entries (#75652) --- .telemetryrc.json | 9 --------- .../telemetry/schema/legacy_oss_plugins.json | 17 ----------------- 2 files changed, 26 deletions(-) delete mode 100644 src/plugins/telemetry/schema/legacy_oss_plugins.json diff --git a/.telemetryrc.json b/.telemetryrc.json index 30643a104c1cd..2f57566159a70 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -1,13 +1,4 @@ [ - { - "output": "src/plugins/telemetry/schema/legacy_oss_plugins.json", - "root": "src/legacy/core_plugins/", - "exclude": [ - "src/legacy/core_plugins/testbed", - "src/legacy/core_plugins/elasticsearch", - "src/legacy/core_plugins/tests_bundle" - ] - }, { "output": "src/plugins/telemetry/schema/oss_plugins.json", "root": "src/plugins/", diff --git a/src/plugins/telemetry/schema/legacy_oss_plugins.json b/src/plugins/telemetry/schema/legacy_oss_plugins.json deleted file mode 100644 index e660ccac9dc36..0000000000000 --- a/src/plugins/telemetry/schema/legacy_oss_plugins.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "properties": { - "csp": { - "properties": { - "strict": { - "type": "boolean" - }, - "warnLegacyBrowsers": { - "type": "boolean" - }, - "rulesChangedFromDefault": { - "type": "boolean" - } - } - } - } -} From 172c464b147bb207b71530fe9c82170ba5d3893f Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 21 Aug 2020 09:02:15 -0700 Subject: [PATCH 05/25] [Enterprise Search] Convert our `public_url` route to `config_data` and collect initialAppData (#75616) * [Setup] DRY out stripTrailingSlash helper - DRYs out repeated code - This will be used by an upcoming server/ endpoint change, hence why it's in common * [Setup] DRY out initial app data types to common/types - In preparation for upcoming server logic that will need to reuse these types + DRY out and clean up workplace_search types - remove unused supportEligible - remove currentUser - unneeded in Kibana * Update callEnterpriseSearchConfigAPI to parse and fetch new expected data * Remove /public_url API for /config_data * Remove getPublicUrl in favor of directly calling the new /config_data API from public/plugin + set returned initialData in this.data * Set up product apps to be passed initial data as props * Fix for Kea/redux state not resetting between AS<->WS nav - resetContext at the top level only gets called once total on first plugin load and never after, causing navigating between WS and AS to crash when both have Kea - this fixes the issue - moves redux Provider to top level app as well * Add very basic Kea logic file to App Search * Finish AppSearchConfigured tests & set up kea+useEffect mocks * [Cleanup] DRY out repeated mock initialAppData to a reusable defaults constant --- .../common/__mocks__/index.ts | 7 ++ .../common/__mocks__/initial_app_data.ts | 45 +++++++++ .../common/strip_trailing_slash/index.test.ts | 17 ++++ .../common/strip_trailing_slash/index.ts | 13 +++ .../common/types/app_search.ts | 25 +++++ .../enterprise_search/common/types/index.ts | 24 +++++ .../common/types/workplace_search.ts | 19 ++++ .../public/applications/__mocks__/kea.mock.ts | 24 +++++ .../__mocks__/shallow_usecontext.mock.ts | 1 + .../applications/app_search/app_logic.test.ts | 35 +++++++ .../applications/app_search/app_logic.ts | 31 ++++++ .../applications/app_search/index.test.tsx | 53 ++++++++-- .../public/applications/app_search/index.tsx | 41 +++++--- .../public/applications/index.tsx | 27 ++++-- .../get_enterprise_search_url.test.ts | 30 ------ .../get_enterprise_search_url.ts | 27 ------ .../shared/enterprise_search_url/index.ts | 1 - .../applications/shared/layout/side_nav.tsx | 3 +- .../overview/__mocks__/overview_logic.mock.ts | 3 +- .../overview/onboarding_steps.test.tsx | 1 - .../overview/overview_logic.test.ts | 9 +- .../components/overview/overview_logic.ts | 9 +- .../applications/workplace_search/index.tsx | 48 ++++------ .../applications/workplace_search/types.ts | 20 +--- .../enterprise_search/public/plugin.ts | 21 ++-- .../lib/enterprise_search_config_api.test.ts | 96 ++++++++++++++++++- .../lib/enterprise_search_config_api.ts | 53 +++++++++- .../enterprise_search/server/plugin.ts | 4 +- ...public_url.test.ts => config_data.test.ts} | 39 +++++--- .../routes/enterprise_search/config_data.ts | 32 +++++++ .../routes/enterprise_search/public_url.ts | 26 ----- 31 files changed, 573 insertions(+), 211 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/common/__mocks__/index.ts create mode 100644 x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts create mode 100644 x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts create mode 100644 x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts create mode 100644 x-pack/plugins/enterprise_search/common/types/app_search.ts create mode 100644 x-pack/plugins/enterprise_search/common/types/index.ts create mode 100644 x-pack/plugins/enterprise_search/common/types/workplace_search.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts rename x-pack/plugins/enterprise_search/server/routes/enterprise_search/{public_url.test.ts => config_data.test.ts} (50%) create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts delete mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/index.ts b/x-pack/plugins/enterprise_search/common/__mocks__/index.ts new file mode 100644 index 0000000000000..57029913fe3a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/__mocks__/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './initial_app_data'; diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts new file mode 100644 index 0000000000000..79e1efc425b4e --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DEFAULT_INITIAL_APP_DATA = { + readOnlyMode: false, + ilmEnabled: true, + configuredLimits: { + maxDocumentByteSize: 102400, + maxEnginesPerMetaEngine: 15, + }, + appSearch: { + accountId: 'some-id-string', + onBoardingComplete: true, + role: { + id: 'account_id:somestring|user_oid:somestring', + roleType: 'owner', + ability: { + accessAllEngines: true, + destroy: ['session'], + manage: ['account_credentials', 'account_engines'], // etc + edit: ['LocoMoco::Account'], // etc + view: ['Engine'], // etc + credentialTypes: ['admin', 'private', 'search'], + availableRoleTypes: ['owner', 'admin'], + }, + }, + }, + workplaceSearch: { + organization: { + name: 'ACME Donuts', + defaultOrgName: 'My Organization', + }, + fpAccount: { + id: 'some-id-string', + groups: ['Default', 'Cats'], + isAdmin: true, + canCreatePersonalSources: true, + isCurated: false, + viewedOnboardingPage: true, + }, + }, +}; diff --git a/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts new file mode 100644 index 0000000000000..b5d64455b1a90 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { stripTrailingSlash } from './'; + +describe('Strip Trailing Slash helper', () => { + it('strips trailing slashes', async () => { + expect(stripTrailingSlash('http://trailing.slash/')).toEqual('http://trailing.slash'); + }); + + it('does nothing is there is no trailing slash', async () => { + expect(stripTrailingSlash('http://ok.url')).toEqual('http://ok.url'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts new file mode 100644 index 0000000000000..ade9bd8742c97 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/strip_trailing_slash/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Small helper for stripping trailing slashes from URLs or paths + * (usually ones that come in from React Router or API endpoints) + */ +export const stripTrailingSlash = (url: string): string => { + return url && url.endsWith('/') ? url.slice(0, -1) : url; +}; diff --git a/x-pack/plugins/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts new file mode 100644 index 0000000000000..5d6ec079e66e0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/app_search.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface IAccount { + accountId: string; + onBoardingComplete: boolean; + role: IRole; +} + +export interface IRole { + id: string; + roleType: string; + ability: { + accessAllEngines: boolean; + destroy: string[]; + manage: string[]; + edit: string[]; + view: string[]; + credentialTypes: string[]; + availableRoleTypes: string[]; + }; +} diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts new file mode 100644 index 0000000000000..52e468b741a07 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { IAccount as IAppSearchAccount } from './app_search'; +import { IAccount as IWorkplaceSearchAccount, IOrganization } from './workplace_search'; + +export interface IInitialAppData { + readOnlyMode?: boolean; + ilmEnabled?: boolean; + configuredLimits?: IConfiguredLimits; + appSearch?: IAppSearchAccount; + workplaceSearch?: { + organization: IOrganization; + fpAccount: IWorkplaceSearchAccount; + }; +} + +export interface IConfiguredLimits { + maxDocumentByteSize: number; + maxEnginesPerMetaEngine: number; +} diff --git a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts new file mode 100644 index 0000000000000..fd8fa6daf81ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface IAccount { + id: string; + groups: string[]; + isAdmin: boolean; + isCurated: boolean; + canCreatePersonalSources: boolean; + viewedOnboardingPage: boolean; +} + +export interface IOrganization { + name: string; + defaultOrgName: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts new file mode 100644 index 0000000000000..5049e9da21ce9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +jest.mock('kea', () => ({ + ...(jest.requireActual('kea') as object), + useValues: jest.fn(() => ({})), + useActions: jest.fn(() => ({})), +})); + +/** + * Example usage within a component test: + * + * import '../../../__mocks__/kea'; // Must come before kea's import, adjust relative path as needed + * + * import { useActions, useValues } from 'kea'; + * + * it('some test', () => { + * (useValues as jest.Mock).mockImplementationOnce(() => ({ someValue: 'hello' })); + * (useActions as jest.Mock).mockImplementationOnce(() => ({ someAction: () => 'world' })); + * }); + */ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts index 792be49a49c48..3a2193db646de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts @@ -14,6 +14,7 @@ import { mockLicenseContext } from './license_context.mock'; jest.mock('react', () => ({ ...(jest.requireActual('react') as object), useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })), + useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior })); /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts new file mode 100644 index 0000000000000..bc31b7df5d971 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; +import { AppLogic } from './app_logic'; + +describe('AppLogic', () => { + beforeEach(() => { + resetContext({}); + AppLogic.mount(); + }); + + const DEFAULT_VALUES = { + hasInitialized: false, + }; + + it('has expected default values', () => { + expect(AppLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('initializeAppData()', () => { + it('sets values based on passed props', () => { + AppLogic.actions.initializeAppData(DEFAULT_INITIAL_APP_DATA); + + expect(AppLogic.values).toEqual({ + hasInitialized: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts new file mode 100644 index 0000000000000..0fb3bb8080d82 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea } from 'kea'; + +import { IInitialAppData } from '../../../common/types'; +import { IKeaLogic } from '../shared/types'; + +export interface IAppLogicValues { + hasInitialized: boolean; +} +export interface IAppLogicActions { + initializeAppData(props: IInitialAppData): void; +} + +export const AppLogic = kea({ + actions: (): IAppLogicActions => ({ + initializeAppData: (props) => props, + }), + reducers: () => ({ + hasInitialized: [ + false, + { + initializeAppData: () => true, + }, + ], + }), +}) as IKeaLogic; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index fa9a761a966e1..0f4072c591bc7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -5,27 +5,68 @@ */ import '../__mocks__/shallow_usecontext.mock'; +import '../__mocks__/kea.mock'; import React, { useContext } from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; +import { useValues, useActions } from 'kea'; +import { SetupGuide } from './components/setup_guide'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { AppSearch, AppSearchNav } from './'; +import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; describe('AppSearch', () => { - it('renders', () => { + it('renders AppSearchUnconfigured when config.host is not set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); - expect(wrapper.find(Layout)).toHaveLength(1); + expect(wrapper.find(AppSearchUnconfigured)).toHaveLength(1); }); - it('redirects to Setup Guide when config.host is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + it('renders AppSearchConfigured when config.host set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'some.url' } })); const wrapper = shallow(); + expect(wrapper.find(AppSearchConfigured)).toHaveLength(1); + }); +}); + +describe('AppSearchUnconfigured', () => { + it('renders the Setup Guide and redirects to the Setup Guide', () => { + const wrapper = shallow(); + + expect(wrapper.find(SetupGuide)).toHaveLength(1); expect(wrapper.find(Redirect)).toHaveLength(1); - expect(wrapper.find(Layout)).toHaveLength(0); + }); +}); + +describe('AppSearchConfigured', () => { + it('renders with layout', () => { + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData: () => {} })); + + const wrapper = shallow(); + + expect(wrapper.find(Layout)).toHaveLength(1); + }); + + it('initializes app data with passed props', () => { + const initializeAppData = jest.fn(); + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); + + shallow(); + + expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true }); + }); + + it('does not re-initialize app data', () => { + const initializeAppData = jest.fn(); + (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); + (useValues as jest.Mock).mockImplementationOnce(() => ({ hasInitialized: true })); + + shallow(); + + expect(initializeAppData).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 5856a13bf75b7..5f4734630624c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -4,13 +4,17 @@ * 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 { Route, Redirect, Switch } from 'react-router-dom'; +import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; -import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { KibanaContext, IKibanaContext } from '../index'; +import { AppLogic, IAppLogicActions, IAppLogicValues } from './app_logic'; +import { IInitialAppData } from '../../../common/types'; + +import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; import { @@ -25,20 +29,29 @@ import { import { SetupGuide } from './components/setup_guide'; import { EngineOverview } from './components/engine_overview'; -export const AppSearch: React.FC = () => { +export const AppSearch: React.FC = (props) => { const { config } = useContext(KibanaContext) as IKibanaContext; + return !config.host ? : ; +}; + +export const AppSearchUnconfigured: React.FC = () => ( + + + + + + + + +); + +export const AppSearchConfigured: React.FC = (props) => { + const { hasInitialized } = useValues(AppLogic) as IAppLogicValues; + const { initializeAppData } = useActions(AppLogic) as IAppLogicActions; - if (!config.host) - return ( - - - - - - - - - ); + useEffect(() => { + if (!hasInitialized) initializeAppData(props); + }, [hasInitialized]); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 1b1f9ae43e7c1..d6cc6e81509b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -8,6 +8,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import { getContext, resetContext } from 'kea'; + import { I18nProvider } from '@kbn/i18n/react'; import { AppMountParameters, @@ -19,6 +23,7 @@ import { import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; import { LicenseProvider } from './shared/licensing'; import { IExternalUrl } from './shared/enterprise_search_url'; +import { IInitialAppData } from '../../common/types'; export interface IKibanaContext { config: { host?: string }; @@ -38,33 +43,41 @@ export const KibanaContext = React.createContext({}); */ export const renderApp = ( - App: React.FC, + App: React.FC, params: AppMountParameters, core: CoreStart, plugins: PluginsSetup, config: ClientConfigType, - data: ClientData + { externalUrl, ...initialData }: ClientData ) => { + resetContext({ createStore: true }); + const store = getContext().store as Store; + ReactDOM.render( - - - + + + + + , params.element ); - return () => ReactDOM.unmountComponentAtNode(params.element); + return () => { + resetContext({}); + ReactDOM.unmountComponentAtNode(params.element); + }; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts deleted file mode 100644 index 42f308c554268..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getPublicUrl } from './'; - -describe('Enterprise Search URL helper', () => { - const httpMock = { get: jest.fn() } as any; - - it('calls and returns the public URL API endpoint', async () => { - httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://some.vanity.url' })); - - expect(await getPublicUrl(httpMock)).toEqual('http://some.vanity.url'); - }); - - it('strips trailing slashes', async () => { - httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://trailing.slash/' })); - - expect(await getPublicUrl(httpMock)).toEqual('http://trailing.slash'); - }); - - // For the most part, error logging/handling is done on the server side. - // On the front-end, we should simply gracefully fall back to config.host - // if we can't fetch a public URL - it('falls back to an empty string', async () => { - expect(await getPublicUrl(httpMock)).toEqual(''); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts deleted file mode 100644 index 419c187a0048a..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts +++ /dev/null @@ -1,27 +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 { HttpSetup } from 'src/core/public'; - -/** - * On Elastic Cloud, the host URL set in kibana.yml is not necessarily the same - * URL we want to send users to in the front-end (e.g. if a vanity URL is set). - * - * This helper checks a Kibana API endpoint (which has checks an Enterprise - * Search internal API endpoint) for the correct public-facing URL to use. - */ -export const getPublicUrl = async (http: HttpSetup): Promise => { - try { - const { publicUrl } = await http.get('/api/enterprise_search/public_url'); - return stripTrailingSlash(publicUrl); - } catch { - return ''; - } -}; - -const stripTrailingSlash = (url: string): string => { - return url.endsWith('/') ? url.slice(0, -1) : url; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts index 563d19f9fdeb5..d2d82a43c6dd9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getPublicUrl } from './get_enterprise_search_url'; export { ExternalUrl, IExternalUrl } from './generate_external_url'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index 5969fa7806a44..72e4f2f091496 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -13,6 +13,7 @@ import { EuiIcon, EuiTitle, EuiText, EuiLink as EuiLinkExternal } from '@elastic import { EuiLink } from '../react_router_helpers'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants'; +import { stripTrailingSlash } from '../../../../common/strip_trailing_slash'; import { NavContext, INavContext } from './layout'; @@ -78,7 +79,7 @@ export const SideNavLink: React.FC = ({ const { closeNavigation } = useContext(NavContext) as INavContext; const { pathname } = useLocation(); - const currentPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; + const currentPath = stripTrailingSlash(pathname); const isActive = currentPath === to || (isRoot && currentPath === ''); const classes = classNames('enterpriseSearchNavLinks__item', className, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts index 43cff5de6668d..395d2044e7dbc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/__mocks__/overview_logic.mock.ts @@ -5,14 +5,13 @@ */ import { IOverviewValues } from '../overview_logic'; -import { IAccount, IOrganization, IUser } from '../../../types'; +import { IAccount, IOrganization } from '../../../types'; export const mockLogicValues = { accountsCount: 0, activityFeed: [], canCreateContentSources: false, canCreateInvitations: false, - currentUser: {} as IUser, fpAccount: {} as IAccount, hasOrgSources: false, hasUsers: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx index 3cf88cf120cc4..acbc66259c2a1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.test.tsx @@ -24,7 +24,6 @@ const account = { isAdmin: true, canCreatePersonalSources: true, groups: [], - supportEligible: true, isCurated: false, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts index 285ec9b973378..7df4de4719f31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.test.ts @@ -31,14 +31,13 @@ describe('OverviewLogic', () => { describe('setServerData', () => { const feed = [{ foo: 'bar' }] as any; - const user = { firstName: 'Joe', email: 'e@e.e', name: 'Joe Jo', color: 'pearl' }; const account = { - name: 'Jane doe', id: '1243', + groups: ['Default'], isAdmin: true, + isCurated: false, canCreatePersonalSources: true, - groups: [], - supportEligible: true, + viewedOnboardingPage: false, }; const org = { name: 'ACME', defaultOrgName: 'Org' }; @@ -47,7 +46,6 @@ describe('OverviewLogic', () => { activityFeed: feed, canCreateContentSources: true, canCreateInvitations: true, - currentUser: user, fpAccount: account, hasOrgSources: true, hasUsers: true, @@ -70,7 +68,6 @@ describe('OverviewLogic', () => { it('will set server values', () => { expect(OverviewLogic.values.organization).toEqual(org); expect(OverviewLogic.values.isFederatedAuth).toEqual(false); - expect(OverviewLogic.values.currentUser).toEqual(user); expect(OverviewLogic.values.fpAccount).toEqual(account); expect(OverviewLogic.values.canCreateInvitations).toEqual(true); expect(OverviewLogic.values.hasUsers).toEqual(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts index f1b4f447f7445..8bb177a2e742b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview_logic.ts @@ -8,7 +8,7 @@ import { HttpSetup } from 'src/core/public'; import { kea } from 'kea'; -import { IAccount, IOrganization, IUser } from '../../types'; +import { IAccount, IOrganization } from '../../types'; import { IFlashMessagesProps, IKeaLogic, TKeaReducers, IKeaParams } from '../../../shared/types'; import { IFeedActivity } from './recent_activity'; @@ -26,7 +26,6 @@ export interface IOverviewServerData { activityFeed: IFeedActivity[]; organization: IOrganization; isFederatedAuth: boolean; - currentUser: IUser; fpAccount: IAccount; } @@ -63,12 +62,6 @@ export const OverviewLogic = kea({ setServerData: (_, { isFederatedAuth }) => isFederatedAuth, }, ], - currentUser: [ - {} as IUser, - { - setServerData: (_, { currentUser }) => currentUser, - }, - ], fpAccount: [ {} as IAccount, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 4aa171a5a5762..94462aa8de7d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -6,14 +6,8 @@ import React, { useContext } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import { Store } from 'redux'; -import { getContext, resetContext } from 'kea'; - -resetContext({ createStore: true }); - -const store = getContext().store as Store; +import { IInitialAppData } from '../../../common/types'; import { KibanaContext, IKibanaContext } from '../index'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav } from './components/layout/nav'; @@ -23,7 +17,7 @@ import { SETUP_GUIDE_PATH } from './routes'; import { SetupGuide } from './components/setup_guide'; import { Overview } from './components/overview'; -export const WorkplaceSearch: React.FC = () => { +export const WorkplaceSearch: React.FC = (props) => { const { config } = useContext(KibanaContext) as IKibanaContext; if (!config.host) return ( @@ -38,25 +32,23 @@ export const WorkplaceSearch: React.FC = () => { ); return ( - - - - - - - - - - }> - - - {/* Will replace with groups component subsequent PR */} -
- - - - - - + + + + + + + + + }> + + + {/* Will replace with groups component subsequent PR */} +
+ + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 77c35adef3300..a8348a6f69a39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -4,24 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IAccount { - id: string; - isCurated?: boolean; - isAdmin: boolean; - canCreatePersonalSources: boolean; - groups: string[]; - supportEligible: boolean; -} - -export interface IOrganization { - name: string; - defaultOrgName: string; -} -export interface IUser { - firstName: string; - email: string; - name: string; - color: string; -} +export * from '../../../common/types/workplace_search'; export type TSpacerSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 0d392eefe0aa2..148a50fb4a5ce 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -20,19 +20,16 @@ import { import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { LicensingPluginSetup } from '../../licensing/public'; +import { IInitialAppData } from '../common/types'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../common/constants'; -import { - getPublicUrl, - ExternalUrl, - IExternalUrl, -} from './applications/shared/enterprise_search_url'; +import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url'; import AppSearchLogo from './applications/app_search/assets/logo.svg'; import WorkplaceSearchLogo from './applications/workplace_search/assets/logo.svg'; export interface ClientConfigType { host?: string; } -export interface ClientData { +export interface ClientData extends IInitialAppData { externalUrl: IExternalUrl; } @@ -119,10 +116,14 @@ export class EnterpriseSearchPlugin implements Plugin { if (!this.config.host) return; // No API to call if (this.hasInitialized) return; // We've already made an initial call - // TODO: Rename to something more generic once we start fetching more data than just external_url from this endpoint - const publicUrl = await getPublicUrl(http); + try { + const { publicUrl, ...initialData } = await http.get('/api/enterprise_search/config_data'); + this.data = { ...this.data, ...initialData }; + if (publicUrl) this.data.externalUrl = new ExternalUrl(publicUrl); - if (publicUrl) this.data.externalUrl = new ExternalUrl(publicUrl); - this.hasInitialized = true; + this.hasInitialized = true; + } catch { + // The plugin will attempt to re-fetch config data on page change + } } } diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index ee96f8099cf7c..c26ada77f504f 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -11,6 +11,7 @@ const { Response } = jest.requireActual('node-fetch'); import { loggingSystemMock } from 'src/core/server/mocks'; +import { DEFAULT_INITIAL_APP_DATA } from '../../common/__mocks__'; import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; describe('callEnterpriseSearchConfigAPI', () => { @@ -35,13 +36,50 @@ describe('callEnterpriseSearchConfigAPI', () => { }, settings: { external_url: 'http://some.vanity.url/', + read_only_mode: false, + ilm_enabled: true, + configured_limits: { + max_document_byte_size: 102400, + max_engines_per_meta_engine: 15, + }, + app_search: { + account_id: 'some-id-string', + onboarding_complete: true, + }, + workplace_search: { + organization: { + name: 'ACME Donuts', + default_org_name: 'My Organization', + }, + fp_account: { + id: 'some-id-string', + groups: ['Default', 'Cats'], + is_admin: true, + can_create_personal_sources: true, + is_curated: false, + viewed_onboarding_page: true, + }, + }, }, - access: { - user: 'someuser', - products: { + current_user: { + name: 'someuser', + access: { app_search: true, workplace_search: false, }, + app_search_role: { + id: 'account_id:somestring|user_oid:somestring', + role_type: 'owner', + ability: { + access_all_engines: true, + destroy: ['session'], + manage: ['account_credentials', 'account_engines'], // etc + edit: ['LocoMoco::Account'], // etc + view: ['Engine'], // etc + credential_types: ['admin', 'private', 'search'], + available_role_types: ['owner', 'admin'], + }, + }, }, }; @@ -56,11 +94,61 @@ describe('callEnterpriseSearchConfigAPI', () => { }); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ - publicUrl: 'http://some.vanity.url/', access: { hasAppSearchAccess: true, hasWorkplaceSearchAccess: false, }, + publicUrl: 'http://some.vanity.url', + ...DEFAULT_INITIAL_APP_DATA, + }); + }); + + it('falls back without error when data is unavailable', async () => { + fetchMock.mockImplementationOnce((url: string) => Promise.resolve(new Response('{}'))); + + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ + access: { + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }, + publicUrl: undefined, + readOnlyMode: false, + ilmEnabled: false, + configuredLimits: { + maxDocumentByteSize: undefined, + maxEnginesPerMetaEngine: undefined, + }, + appSearch: { + accountId: undefined, + onBoardingComplete: false, + role: { + id: undefined, + roleType: undefined, + ability: { + accessAllEngines: false, + destroy: [], + manage: [], + edit: [], + view: [], + credentialTypes: [], + availableRoleTypes: [], + }, + }, + }, + workplaceSearch: { + organization: { + name: undefined, + defaultOrgName: undefined, + }, + fpAccount: { + id: undefined, + groups: [], + isAdmin: false, + canCreatePersonalSources: false, + isCurated: false, + viewedOnboardingPage: false, + }, + }, }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 7a6d1eac1b454..1dbec76806ba8 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -11,14 +11,17 @@ import { KibanaRequest, Logger } from 'src/core/server'; import { ConfigType } from '../'; import { IAccess } from './check_access'; +import { IInitialAppData } from '../../common/types'; +import { stripTrailingSlash } from '../../common/strip_trailing_slash'; + interface IParams { request: KibanaRequest; config: ConfigType; log: Logger; } -interface IReturn { - publicUrl?: string; +interface IReturn extends IInitialAppData { access?: IAccess; + publicUrl?: string; } /** @@ -57,10 +60,50 @@ export const callEnterpriseSearchConfigAPI = async ({ const data = await response.json(); return { - publicUrl: data?.settings?.external_url, access: { - hasAppSearchAccess: !!data?.access?.products?.app_search, - hasWorkplaceSearchAccess: !!data?.access?.products?.workplace_search, + hasAppSearchAccess: !!data?.current_user?.access?.app_search, + hasWorkplaceSearchAccess: !!data?.current_user?.access?.workplace_search, + }, + publicUrl: stripTrailingSlash(data?.settings?.external_url), + readOnlyMode: !!data?.settings?.read_only_mode, + ilmEnabled: !!data?.settings?.ilm_enabled, + configuredLimits: { + maxDocumentByteSize: data?.settings?.configured_limits?.max_document_byte_size, + maxEnginesPerMetaEngine: data?.settings?.configured_limits?.max_engines_per_meta_engine, + }, + appSearch: { + accountId: data?.settings?.app_search?.account_id, + onBoardingComplete: !!data?.settings?.app_search?.onboarding_complete, + role: { + id: data?.current_user?.app_search_role?.id, + roleType: data?.current_user?.app_search_role?.role_type, + ability: { + accessAllEngines: !!data?.current_user?.app_search_role?.ability?.access_all_engines, + destroy: data?.current_user?.app_search_role?.ability?.destroy || [], + manage: data?.current_user?.app_search_role?.ability?.manage || [], + edit: data?.current_user?.app_search_role?.ability?.edit || [], + view: data?.current_user?.app_search_role?.ability?.view || [], + credentialTypes: data?.current_user?.app_search_role?.ability?.credential_types || [], + availableRoleTypes: + data?.current_user?.app_search_role?.ability?.available_role_types || [], + }, + }, + }, + workplaceSearch: { + organization: { + name: data?.settings?.workplace_search?.organization?.name, + defaultOrgName: data?.settings?.workplace_search?.organization?.default_org_name, + }, + fpAccount: { + id: data?.settings?.workplace_search?.fp_account.id, + groups: data?.settings?.workplace_search?.fp_account.groups || [], + isAdmin: !!data?.settings?.workplace_search?.fp_account?.is_admin, + canCreatePersonalSources: !!data?.settings?.workplace_search?.fp_account + ?.can_create_personal_sources, + isCurated: !!data?.settings?.workplace_search?.fp_account.is_curated, + viewedOnboardingPage: !!data?.settings?.workplace_search?.fp_account + .viewed_onboarding_page, + }, }, }; } catch (err) { diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 6de6671337797..770ea8d420c20 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -26,7 +26,7 @@ import { } from '../common/constants'; import { ConfigType } from './'; import { checkAccess } from './lib/check_access'; -import { registerPublicUrlRoute } from './routes/enterprise_search/public_url'; +import { registerConfigDataRoute } from './routes/enterprise_search/config_data'; import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; @@ -105,7 +105,7 @@ export class EnterpriseSearchPlugin implements Plugin { const router = http.createRouter(); const dependencies = { router, config, log: this.logger }; - registerPublicUrlRoute(dependencies); + registerConfigDataRoute(dependencies); registerEnginesRoute(dependencies); registerWSOverviewRoute(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts similarity index 50% rename from x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts rename to x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts index 846aae3fce56f..7484e27594df4 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { MockRouter, mockDependencies } from '../__mocks__'; jest.mock('../../lib/enterprise_search_config_api', () => ({ @@ -11,41 +12,51 @@ jest.mock('../../lib/enterprise_search_config_api', () => ({ })); import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; -import { registerPublicUrlRoute } from './public_url'; +import { registerConfigDataRoute } from './config_data'; -describe('Enterprise Search Public URL API', () => { +describe('Enterprise Search Config Data API', () => { let mockRouter: MockRouter; beforeEach(() => { mockRouter = new MockRouter({ method: 'get' }); - registerPublicUrlRoute({ + registerConfigDataRoute({ ...mockDependencies, router: mockRouter.router, }); }); - describe('GET /api/enterprise_search/public_url', () => { - it('returns a publicUrl', async () => { + describe('GET /api/enterprise_search/config_data', () => { + it('returns an initial set of config data from Enterprise Search', async () => { + const mockData = { + access: { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }, + publicUrl: 'http://localhost:3002', + ...DEFAULT_INITIAL_APP_DATA, + }; + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ publicUrl: 'http://some.vanity.url' }); + return Promise.resolve(mockData); }); - await mockRouter.callRoute({}); expect(mockRouter.response.ok).toHaveBeenCalledWith({ - body: { publicUrl: 'http://some.vanity.url' }, + body: mockData, headers: { 'content-type': 'application/json' }, }); }); - // For the most part, all error logging is handled by callEnterpriseSearchConfigAPI. - // This endpoint should mostly just fall back gracefully to an empty string - it('falls back to an empty string', async () => { + it('throws a 502 error if data returns an empty obj', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve({}); + }); await mockRouter.callRoute({}); - expect(mockRouter.response.ok).toHaveBeenCalledWith({ - body: { publicUrl: '' }, - headers: { 'content-type': 'application/json' }, + + expect(mockRouter.response.customError).toHaveBeenCalledWith({ + statusCode: 502, + body: 'Error fetching data from Enterprise Search', }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts new file mode 100644 index 0000000000000..453c7fd99bf4c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouteDependencies } from '../../plugin'; +import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; + +export function registerConfigDataRoute({ router, config, log }: IRouteDependencies) { + router.get( + { + path: '/api/enterprise_search/config_data', + validate: false, + }, + async (context, request, response) => { + const data = await callEnterpriseSearchConfigAPI({ request, config, log }); + + if (!Object.keys(data).length) { + return response.customError({ + statusCode: 502, + body: 'Error fetching data from Enterprise Search', + }); + } else { + return response.ok({ + body: data, + headers: { 'content-type': 'application/json' }, + }); + } + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts deleted file mode 100644 index a9edd4eb10da0..0000000000000 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts +++ /dev/null @@ -1,26 +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 { IRouteDependencies } from '../../plugin'; -import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; - -export function registerPublicUrlRoute({ router, config, log }: IRouteDependencies) { - router.get( - { - path: '/api/enterprise_search/public_url', - validate: false, - }, - async (context, request, response) => { - const { publicUrl = '' } = - (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; - - return response.ok({ - body: { publicUrl }, - headers: { 'content-type': 'application/json' }, - }); - } - ); -} From 471b11408929cb58b3fc10f886bef220f93378d6 Mon Sep 17 00:00:00 2001 From: DeDe Morton Date: Fri, 21 Aug 2020 09:08:07 -0700 Subject: [PATCH 06/25] [DOCS] Update links to Beats documentation (#70380) * Update links to Beats documentation * Update snapshot files * Fix lint errors --- docs/management/watcher-ui/index.asciidoc | 2 +- .../monitoring/monitoring-metricbeat.asciidoc | 2 +- .../public/doc_links/doc_links_service.ts | 4 ++-- .../server/tutorials/cloudwatch_logs/index.ts | 3 ++- .../instructions/auditbeat_instructions.ts | 16 ++++++------- .../instructions/filebeat_instructions.ts | 16 ++++++------- .../instructions/functionbeat_instructions.ts | 17 ++++++++----- .../instructions/heartbeat_instructions.ts | 21 ++++++++-------- .../instructions/metricbeat_instructions.ts | 23 +++++++++++------- .../instructions/winlogbeat_instructions.ts | 5 ++-- .../server/tutorials/uptime_monitors/index.ts | 2 +- .../logs/__snapshots__/reason.test.js.snap | 4 ++-- .../public/components/logs/reason.js | 4 ++-- .../flyout/__snapshots__/flyout.test.js.snap | 24 +++++++++---------- .../apm/enable_metricbeat_instructions.js | 4 ++-- .../beats/enable_metricbeat_instructions.js | 4 ++-- .../enable_metricbeat_instructions.js | 2 +- .../kibana/enable_metricbeat_instructions.js | 2 +- .../enable_metricbeat_instructions.js | 2 +- 19 files changed, 86 insertions(+), 71 deletions(-) diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index fbe5fcd5cd3a5..23a0acbff5718 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -60,7 +60,7 @@ The following example walks you through creating a threshold alert. The alert is triggered when the maximum total CPU usage on a machine goes above a certain percentage. The example uses https://www.elastic.co/products/beats/metricbeat[Metricbeat] to collect metrics from your systems and services. -{metricbeat-ref}/metricbeat-installation.html[Learn more] on how to install +{metricbeat-ref}/metricbeat-installation-configuration.html[Learn more] on how to install and get started with Metricbeat. [float] diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index d18ebe95c7974..5ef3b8177a9c5 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -82,7 +82,7 @@ For more information, see {ref}/monitoring-settings.html[Monitoring settings in and {ref}/cluster-update-settings.html[Cluster update settings]. -- -. {metricbeat-ref}/metricbeat-installation.html[Install {metricbeat}] on the +. {metricbeat-ref}/metricbeat-installation-configuration.html[Install {metricbeat}] on the same server as {kib}. . Enable the {kib} {xpack} module in {metricbeat}. + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index bd279baa78d98..fc753517fd940 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -41,8 +41,8 @@ export class DocLinksService { }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, - installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation.html`, - configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-configuration.html`, + installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation-configuration.html`, + configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/configuring-howto-filebeat.html`, elasticsearchOutput: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/elasticsearch-output.html`, startup: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-starting.html`, exportedFields: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/exported-fields.html`, diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index fb7b07c5dc1af..6b017fae1e21f 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -47,7 +47,8 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc an AWS Lambda function. \ [Learn more]({learnMoreLink}).', values: { - learnMoreLink: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html', + learnMoreLink: + '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', }, }), euiIconType: 'logoAWS', diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index 2a6cfa0358709..b6f7aa8c53ac9 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -31,9 +31,9 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Auditbeat', }), textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', }, }), commands: [ @@ -47,9 +47,9 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Auditbeat', }), textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', }, }), commands: [ @@ -68,9 +68,9 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Auditbeat', }), textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', }, }), commands: [ @@ -92,7 +92,7 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.auditbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Auditbeat? See the [Getting Started Guide]({guideLinkUrl}).\n\ + 'First time using Auditbeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Auditbeat Windows zip file from the [Download]({auditbeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Auditbeat`.\n\ @@ -101,7 +101,7 @@ export const createAuditbeatInstructions = (context?: TutorialContext) => ({ 5. From the PowerShell prompt, run the following commands to install Auditbeat as a Windows service.', values: { folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.auditbeat}/auditbeat-getting-started.html', + guideLinkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', auditbeatLinkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', directoryName: 'auditbeat-{config.kibana.version}-windows', }, diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index 0e99033b2ea69..c760840165bfc 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -31,9 +31,9 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Filebeat', }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', }, }), commands: [ @@ -47,9 +47,9 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Filebeat', }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', }, }), commands: [ @@ -68,9 +68,9 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Filebeat', }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', }, }), commands: [ @@ -90,7 +90,7 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ }), textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Filebeat? See the [Getting Started Guide]({guideLinkUrl}).\n\ + 'First time using Filebeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Filebeat Windows zip file from the [Download]({filebeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Filebeat`.\n\ @@ -99,7 +99,7 @@ export const createFilebeatInstructions = (context?: TutorialContext) => ({ 5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', values: { folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', directoryName: 'filebeat-{config.kibana.version}-windows', }, diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index 06ff84146b5d8..61e76bd9d3c18 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -31,8 +31,10 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Functionbeat', }), textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Functionbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html' }, + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -47,8 +49,10 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ textPre: i18n.translate( 'home.tutorials.common.functionbeatInstructions.install.linuxTextPre', { - defaultMessage: 'First time using Functionbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html' }, + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, } ), commands: [ @@ -65,7 +69,7 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.functionbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Functionbeat? See the [Getting Started Guide]({functionbeatLink}).\n\ + 'First time using Functionbeat? See the [Quick Start]({functionbeatLink}).\n\ 1. Download the Functionbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Functionbeat`.\n\ @@ -75,7 +79,8 @@ export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`functionbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - functionbeatLink: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html', + functionbeatLink: + '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/functionbeat', }, } diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index fa5bf5df13b6b..4d519ad8aa01e 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -31,8 +31,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Heartbeat', }), textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html' }, + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -45,8 +45,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Heartbeat', }), textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html' }, + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-amd64.deb', @@ -62,8 +62,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Heartbeat', }), textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html' }, + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-x86_64.rpm', @@ -82,7 +82,7 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.heartbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Heartbeat? See the [Getting Started Guide]({heartbeatLink}).\n\ + 'First time using Heartbeat? See the [Quick Start]({heartbeatLink}).\n\ 1. Download the Heartbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Heartbeat`.\n\ @@ -92,7 +92,8 @@ export const createHeartbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`heartbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - heartbeatLink: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html', + heartbeatLink: + '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/heartbeat', }, } @@ -357,7 +358,7 @@ export function heartbeatEnableInstructionsOnPrem() { 'Where {hostTemplate} is your monitored URL, For more details on how to configure Monitors in \ Heartbeat, read the [Heartbeat configuration docs.]({configureLink})', values: { - configureLink: '{config.docs.beats.heartbeat}/heartbeat-configuration.html', + configureLink: '{config.docs.beats.heartbeat}/configuring-howto-heartbeat.html', hostTemplate: '``', }, } @@ -428,7 +429,7 @@ export function heartbeatEnableInstructionsCloud() { { defaultMessage: 'For more details on how to configure Monitors in Heartbeat, read the [Heartbeat configuration docs.]({configureLink})', - values: { configureLink: '{config.docs.beats.heartbeat}/heartbeat-configuration.html' }, + values: { configureLink: '{config.docs.beats.heartbeat}/configuring-howto-heartbeat.html' }, } ); return { diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index 651405941610f..cce93e0dfb527 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -31,8 +31,10 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Metricbeat', }), textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -45,8 +47,10 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Metricbeat', }), textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', @@ -62,8 +66,10 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ defaultMessage: 'Download and install Metricbeat', }), textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', - values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', @@ -82,7 +88,7 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.metricbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Metricbeat? See the [Getting Started Guide]({metricbeatLink}).\n\ + 'First time using Metricbeat? See the [Quick Start]({metricbeatLink}).\n\ 1. Download the Metricbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Metricbeat`.\n\ @@ -92,7 +98,8 @@ export const createMetricbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`metricbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - metricbeatLink: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html', + metricbeatLink: + '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', }, } diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index 27d7822e080a3..1eacbb729aee4 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -34,7 +34,7 @@ export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPre', { defaultMessage: - 'First time using Winlogbeat? See the [Getting Started Guide]({winlogbeatLink}).\n\ + 'First time using Winlogbeat? See the [Quick Start]({winlogbeatLink}).\n\ 1. Download the Winlogbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Winlogbeat`.\n\ @@ -44,7 +44,8 @@ export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ values: { directoryName: '`winlogbeat-{config.kibana.version}-windows`', folderPath: '`C:\\Program Files`', - winlogbeatLink: '{config.docs.beats.winlogbeat}/winlogbeat-getting-started.html', + winlogbeatLink: + '{config.docs.beats.winlogbeat}/winlogbeat-installation-configuration.html', elasticLink: 'https://www.elastic.co/downloads/beats/winlogbeat', }, } diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index 7366583e59778..96b81c9fb4181 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -47,7 +47,7 @@ export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSc Given a list of URLs, Heartbeat asks the simple question: Are you alive? \ [Learn more]({learnMoreLink}).', values: { - learnMoreLink: '{config.docs.beats.heartbeat}/heartbeat-getting-started.html', + learnMoreLink: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', }, }), euiIconType: 'uptimeApp', diff --git a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap index b63fe7047e96c..c925ecd1c98ff 100644 --- a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap @@ -13,7 +13,7 @@ exports[`Logs should render a default message 1`] = ` values={ Object { "link": Filebeat diff --git a/x-pack/plugins/monitoring/public/components/logs/reason.js b/x-pack/plugins/monitoring/public/components/logs/reason.js index ad21f7f81d9bd..55dca72bf645d 100644 --- a/x-pack/plugins/monitoring/public/components/logs/reason.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.js @@ -24,7 +24,7 @@ export const Reason = ({ reason }) => { link: ( { link: ( {i18n.translate('xpack.monitoring.logs.reason.noIndexPatternLink', { defaultMessage: 'Filebeat', diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap index c5507efb989de..2f29cd9122a61 100644 --- a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap @@ -156,7 +156,7 @@ exports[`Flyout apm part two should show instructions to migrate to metricbeat 1 "children":

Date: Fri, 21 Aug 2020 18:08:25 +0200 Subject: [PATCH 07/25] [Lens] Register saved object references (#74523) --- x-pack/plugins/lens/common/types.ts | 10 + .../lens/public/app_plugin/app.test.tsx | 137 +++++++---- x-pack/plugins/lens/public/app_plugin/app.tsx | 92 ++++--- .../visualization.test.tsx | 19 +- .../datatable_visualization/visualization.tsx | 11 +- .../__mocks__/expression_helpers.ts | 14 ++ .../editor_frame/config_panel/layer_panel.tsx | 1 - .../editor_frame/editor_frame.test.tsx | 109 +++------ .../editor_frame/editor_frame.tsx | 116 ++++----- .../editor_frame/expression_helpers.ts | 60 +---- .../editor_frame/save.test.ts | 37 ++- .../editor_frame_service/editor_frame/save.ts | 73 +++--- .../editor_frame/state_helpers.ts | 87 +++++++ .../editor_frame/state_management.test.ts | 8 +- .../editor_frame/suggestion_helpers.ts | 2 +- .../editor_frame/suggestion_panel.test.tsx | 1 - .../editor_frame/suggestion_panel.tsx | 41 ++-- .../workspace_panel/chart_switch.tsx | 2 +- .../workspace_panel/workspace_panel.test.tsx | 19 +- .../workspace_panel/workspace_panel.tsx | 21 +- .../embeddable/embeddable.test.tsx | 56 ++++- .../embeddable/embeddable.tsx | 60 ++++- .../embeddable/embeddable_factory.ts | 15 +- .../embeddable/expression_wrapper.tsx | 13 +- .../public/editor_frame_service/mocks.tsx | 4 +- .../public/editor_frame_service/service.tsx | 20 +- .../__mocks__/loader.ts | 4 + .../indexpattern.test.ts | 107 ++++---- .../indexpattern_datasource/indexpattern.tsx | 34 ++- .../indexpattern_datasource/loader.test.ts | 86 ++++++- .../public/indexpattern_datasource/loader.ts | 54 ++++- .../public/indexpattern_datasource/types.ts | 9 +- .../metric_visualization.test.ts | 9 +- .../metric_visualization.tsx | 16 +- .../lens/public/metric_visualization/types.ts | 2 - .../persistence/filter_references.test.ts | 99 ++++++++ .../public/persistence/filter_references.ts | 56 +++++ .../plugins/lens/public/persistence/index.ts | 1 + .../persistence/saved_object_store.test.ts | 51 ++-- .../public/persistence/saved_object_store.ts | 37 +-- .../pie_visualization/pie_visualization.tsx | 4 +- .../public/pie_visualization/to_expression.ts | 20 +- x-pack/plugins/lens/public/types.ts | 37 ++- .../xy_visualization/to_expression.test.ts | 16 +- .../public/xy_visualization/to_expression.ts | 32 +-- .../lens/public/xy_visualization/types.ts | 1 - .../xy_visualization/xy_visualization.test.ts | 6 - .../xy_visualization/xy_visualization.tsx | 6 +- .../__snapshots__/migrations.test.ts.snap | 188 ++++++++++++++ x-pack/plugins/lens/server/migrations.test.ts | 229 ++++++++++++++++++ x-pack/plugins/lens/server/migrations.ts | 136 ++++++++++- 51 files changed, 1609 insertions(+), 659 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts create mode 100644 x-pack/plugins/lens/public/persistence/filter_references.test.ts create mode 100644 x-pack/plugins/lens/public/persistence/filter_references.ts create mode 100644 x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 56a56bdc2d59c..c572b59899fce 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FilterMeta, Filter } from 'src/plugins/data/common'; + export interface ExistingFields { indexPatternTitle: string; existingFieldNames: string[]; @@ -13,3 +15,11 @@ export interface DateRange { fromDate: string; toDate: string; } + +export interface PersistableFilterMeta extends FilterMeta { + indexRefName?: string; +} + +export interface PersistableFilter extends Filter { + meta: PersistableFilterMeta; +} diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 70136a486e8c1..442f82161512f 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -33,7 +33,7 @@ import { navigationPluginMock } from '../../../../../src/plugins/navigation/publ import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { coreMock } from 'src/core/public/mocks'; -jest.mock('../persistence'); +jest.mock('../editor_frame_service/editor_frame/expression_helpers'); jest.mock('src/core/public'); jest.mock('../../../../../src/plugins/saved_objects/public', () => { // eslint-disable-next-line no-shadow @@ -284,11 +284,11 @@ describe('Lens App', () => { (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', title: 'Daaaaaaadaumching!', - expression: 'valid expression', state: { query: 'fake query', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, + references: [], }); await act(async () => { instance.setProps({ docId: '1234' }); @@ -346,12 +346,11 @@ describe('Lens App', () => { args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', - expression: 'valid expression', state: { query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], }); instance = mount(); @@ -375,15 +374,13 @@ describe('Lens App', () => { expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - doc: { + doc: expect.objectContaining({ id: '1234', - expression: 'valid expression', - state: { + state: expect.objectContaining({ query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, - }, - }, + }), + }), }) ); }); @@ -444,7 +441,6 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, } as jest.ResolvedValue); }); @@ -467,7 +463,12 @@ describe('Lens App', () => { } async function save({ - lastKnownDoc = { expression: 'kibana 3' }, + lastKnownDoc = { + references: [], + state: { + filters: [], + }, + }, initialDocId, ...saveProps }: SaveProps & { @@ -481,16 +482,14 @@ describe('Lens App', () => { args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', - expression: 'kibana', + references: [], state: { query: 'fake query', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, filters: [], }, }); (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ id: id || 'aaa', - expression: 'kibana 2', })); await act(async () => { @@ -508,6 +507,7 @@ describe('Lens App', () => { onChange({ filterableIndexPatterns: [], doc: { id: initialDocId, ...lastKnownDoc } as Document, + isSaveable: true, }) ); @@ -541,7 +541,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + doc: ({ id: 'will save this' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -560,7 +561,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + doc: ({ id: 'will save this' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -575,11 +577,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - id: undefined, - title: 'hello there', - expression: 'kibana 3', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: undefined, + title: 'hello there', + }) + ); expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); @@ -595,11 +598,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - id: undefined, - title: 'hello there', - expression: 'kibana 3', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: undefined, + title: 'hello there', + }) + ); expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); @@ -615,11 +619,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - id: '1234', - title: 'hello there', - expression: 'kibana 3', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: '1234', + title: 'hello there', + }) + ); expect(args.redirectTo).not.toHaveBeenCalled(); @@ -639,7 +644,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }) ); @@ -663,11 +669,12 @@ describe('Lens App', () => { newTitle: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ - expression: 'kibana 3', - id: undefined, - title: 'hello there', - }); + expect(args.docStorage.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: undefined, + title: 'hello there', + }) + ); expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, true); }); @@ -717,7 +724,8 @@ describe('Lens App', () => { await act(async () => onChange({ filterableIndexPatterns: [], - doc: ({ id: '123', expression: 'valid expression' } as unknown) as Document, + doc: ({ id: '123' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -756,7 +764,8 @@ describe('Lens App', () => { await act(async () => onChange({ filterableIndexPatterns: [], - doc: ({ expression: 'valid expression' } as unknown) as Document, + doc: ({} as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -779,7 +788,6 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, } as jest.ResolvedValue); }); @@ -824,8 +832,9 @@ describe('Lens App', () => { await act(async () => { onChange({ - filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + filterableIndexPatterns: ['1'], + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }); }); @@ -842,8 +851,9 @@ describe('Lens App', () => { await act(async () => { onChange({ - filterableIndexPatterns: [{ id: '2', title: 'second index' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + filterableIndexPatterns: ['2'], + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }); }); @@ -1078,11 +1088,11 @@ describe('Lens App', () => { (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234', title: 'My cool doc', - expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, + references: [], } as jest.ResolvedValue); }); @@ -1114,7 +1124,12 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + doc: ({ + id: undefined, + + references: [], + } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1135,7 +1150,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + doc: ({ id: undefined, state: {} } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1159,7 +1175,12 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'different expression' } as unknown) as Document, + doc: ({ + id: '1234', + + references: [], + } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1183,7 +1204,16 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, + doc: ({ + id: '1234', + title: 'My cool doc', + references: [], + state: { + query: 'kuery', + filters: [], + }, + } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1207,7 +1237,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: null } as unknown) as Document, + doc: ({ id: '1234', references: [] } as unknown) as Document, + isSaveable: true, }) ); instance.update(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 5ca6f27a0c578..021ca8b182b2b 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -28,7 +28,7 @@ import { OnSaveProps, checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; -import { Document, SavedObjectStore } from '../persistence'; +import { Document, SavedObjectStore, injectFilterReferences } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -57,6 +57,7 @@ interface State { query: Query; filters: Filter[]; savedQuery?: SavedQuery; + isSaveable: boolean; } export function App({ @@ -100,6 +101,7 @@ export function App({ originatingApp, filters: data.query.filterManager.getFilters(), indicateNoData: false, + isSaveable: false, }; }); @@ -122,11 +124,7 @@ export function App({ const { lastKnownDoc } = state; - const isSaveable = - lastKnownDoc && - lastKnownDoc.expression && - lastKnownDoc.expression.length > 0 && - core.application.capabilities.visualize.save; + const savingPermitted = state.isSaveable && core.application.capabilities.visualize.save; useEffect(() => { // Clear app-specific filters when navigating to Lens. Necessary because Lens @@ -177,15 +175,34 @@ export function App({ history, ]); + const getLastKnownDocWithoutPinnedFilters = useCallback( + function () { + if (!lastKnownDoc) return undefined; + const [pinnedFilters, appFilters] = _.partition( + injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), + esFilters.isFilterPinned + ); + return pinnedFilters?.length + ? { + ...lastKnownDoc, + state: { + ...lastKnownDoc.state, + filters: appFilters, + }, + } + : lastKnownDoc; + }, + [lastKnownDoc] + ); + useEffect(() => { onAppLeave((actions) => { // Confirm when the user has made any changes to an existing doc // or when the user has configured something without saving if ( core.application.capabilities.visualize.save && - (state.persistedDoc?.expression - ? !_.isEqual(lastKnownDoc?.expression, state.persistedDoc.expression) - : lastKnownDoc?.expression) + !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) && + (state.isSaveable || state.persistedDoc) ) { return actions.confirm( i18n.translate('xpack.lens.app.unsavedWorkMessage', { @@ -199,7 +216,14 @@ export function App({ return actions.default(); } }); - }, [lastKnownDoc, onAppLeave, state.persistedDoc, core.application.capabilities.visualize.save]); + }, [ + lastKnownDoc, + onAppLeave, + state.persistedDoc, + state.isSaveable, + core.application.capabilities.visualize.save, + getLastKnownDocWithoutPinnedFilters, + ]); // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { @@ -248,13 +272,17 @@ export function App({ .load(docId) .then((doc) => { getAllIndexPatterns( - doc.state.datasourceMetaData.filterableIndexPatterns, + _.uniq( + doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ), data.indexPatterns, core.notifications ) .then((indexPatterns) => { // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters(doc.state.filters); + data.query.filterManager.setAppFilters( + injectFilterReferences(doc.state.filters, doc.references) + ); setState((s) => ({ ...s, isLoading: false, @@ -264,13 +292,13 @@ export function App({ indexPatternsForTopNav: indexPatterns, })); }) - .catch(() => { + .catch((e) => { setState((s) => ({ ...s, isLoading: false })); redirectTo(); }); }) - .catch(() => { + .catch((e) => { setState((s) => ({ ...s, isLoading: false })); core.notifications.toasts.addDanger( @@ -306,22 +334,9 @@ export function App({ if (!lastKnownDoc) { return; } - const [pinnedFilters, appFilters] = _.partition( - lastKnownDoc.state?.filters, - esFilters.isFilterPinned - ); - const lastDocWithoutPinned = pinnedFilters?.length - ? { - ...lastKnownDoc, - state: { - ...lastKnownDoc.state, - filters: appFilters, - }, - } - : lastKnownDoc; const doc = { - ...lastDocWithoutPinned, + ...getLastKnownDocWithoutPinnedFilters()!, description: saveProps.newDescription, id: saveProps.newCopyOnSave ? undefined : lastKnownDoc.id, title: saveProps.newTitle, @@ -411,7 +426,7 @@ export function App({ emphasize: true, iconType: 'check', run: () => { - if (isSaveable && lastKnownDoc) { + if (savingPermitted) { runSave({ newTitle: lastKnownDoc.title, newCopyOnSave: false, @@ -421,7 +436,7 @@ export function App({ } }, testId: 'lnsApp_saveAndReturnButton', - disableButton: !isSaveable, + disableButton: !savingPermitted, }, ] : []), @@ -436,12 +451,12 @@ export function App({ }), emphasize: !state.originatingApp || !lastKnownDoc?.id, run: () => { - if (isSaveable && lastKnownDoc) { + if (savingPermitted) { setState((s) => ({ ...s, isSaveModalVisible: true })); } }, testId: 'lnsApp_saveButton', - disableButton: !isSaveable, + disableButton: !savingPermitted, }, ]} data-test-subj="lnsApp_topNav" @@ -522,7 +537,10 @@ export function App({ doc: state.persistedDoc, onError, showNoDataPopover, - onChange: ({ filterableIndexPatterns, doc }) => { + onChange: ({ filterableIndexPatterns, doc, isSaveable }) => { + if (isSaveable !== state.isSaveable) { + setState((s) => ({ ...s, isSaveable })); + } if (!_.isEqual(state.persistedDoc, doc)) { setState((s) => ({ ...s, lastKnownDoc: doc })); } @@ -530,8 +548,8 @@ export function App({ // Update the cached index patterns if the user made a change to any of them if ( state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.find( - ({ id }) => + filterableIndexPatterns.some( + (id) => !state.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) ) ) { @@ -573,12 +591,12 @@ export function App({ } export async function getAllIndexPatterns( - ids: Array<{ id: string }>, + ids: string[], indexPatternsService: IndexPatternsContract, notifications: NotificationsStart ): Promise { try { - return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id))); + return await Promise.all(ids.map((id) => indexPatternsService.get(id))); } catch (e) { notifications.toasts.addDanger( i18n.translate('xpack.lens.app.indexPatternLoadingError', { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 0b6584277ffa7..194f12cf9291b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -50,20 +50,6 @@ describe('Datatable Visualization', () => { }); }); - describe('#getPersistableState', () => { - it('should persist the internal state', () => { - const expectedState: DatatableVisualizationState = { - layers: [ - { - layerId: 'baz', - columns: ['a', 'b', 'c'], - }, - ], - }; - expect(datatableVisualization.getPersistableState(expectedState)).toEqual(expectedState); - }); - }); - describe('#getLayerIds', () => { it('return the layer ids', () => { const state: DatatableVisualizationState = { @@ -340,7 +326,10 @@ describe('Datatable Visualization', () => { label: 'label', }); - const expression = datatableVisualization.toExpression({ layers: [layer] }, frame) as Ast; + const expression = datatableVisualization.toExpression( + { layers: [layer] }, + frame.datasourceLayers + ) as Ast; const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns'); expect(tableArgs).toHaveLength(1); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 659f8ea12bcb0..5aff4e14b17f2 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -25,10 +25,7 @@ function newLayerState(layerId: string): LayerState { }; } -export const datatableVisualization: Visualization< - DatatableVisualizationState, - DatatableVisualizationState -> = { +export const datatableVisualization: Visualization = { id: 'lnsDatatable', visualizationTypes: [ @@ -75,8 +72,6 @@ export const datatableVisualization: Visualization< ); }, - getPersistableState: (state) => state, - getSuggestions({ table, state, @@ -186,9 +181,9 @@ export const datatableVisualization: Visualization< }; }, - toExpression(state, frame): Ast { + toExpression(state, datasourceLayers): Ast { const layer = state.layers[0]; - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); // When we add a column it could be empty, and therefore have no order const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts new file mode 100644 index 0000000000000..e0b3616315cbd --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Ast } from '@kbn/interpreter/common'; + +export function buildExpression(): Ast { + return { + type: 'expression', + chain: [{ type: 'function', function: 'test', arguments: {} }], + }; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 38224bf962a3f..b2804cfddba58 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -124,7 +124,6 @@ export function LayerPanel( const nextPublicAPI = layerDatasource.getPublicAPI({ state: newState, layerId, - dateRange: props.framePublicAPI.dateRange, }); const nextTable = new Set( nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 2f7a78197b2b2..e628ea0675a8d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -170,25 +170,22 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: datasource1State, testDatasource2: datasource2State, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); }); - expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State); - expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State); + expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, []); + expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, []); expect(mockDatasource3.initialize).not.toHaveBeenCalled(); }); @@ -425,21 +422,6 @@ describe('editor_frame', () => { "function": "kibana", "type": "function", }, - Object { - "arguments": Object { - "filters": Array [ - "[]", - ], - "query": Array [ - "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", - ], - "timeRange": Array [ - "{\\"from\\":\\"\\",\\"to\\":\\"\\"}", - ], - }, - "function": "kibana_context", - "type": "function", - }, Object { "arguments": Object { "layerIds": Array [ @@ -499,19 +481,16 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: {}, testDatasource2: {}, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -535,21 +514,6 @@ describe('editor_frame', () => { "function": "kibana", "type": "function", }, - Object { - "arguments": Object { - "filters": Array [ - "[]", - ], - "query": Array [ - "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", - ], - "timeRange": Array [ - "{\\"from\\":\\"\\",\\"to\\":\\"\\"}", - ], - }, - "function": "kibana_context", - "type": "function", - }, Object { "arguments": Object { "layerIds": Array [ @@ -747,19 +711,16 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: {}, testDatasource2: {}, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -802,19 +763,16 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: datasource1State, testDatasource2: datasource2State, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -842,7 +800,6 @@ describe('editor_frame', () => { it('should give access to the datasource state in the datasource factory function', async () => { const datasourceState = {}; - const dateRange = { fromDate: 'now-1w', toDate: 'now' }; mockDatasource.initialize.mockResolvedValue(datasourceState); mockDatasource.getLayers.mockReturnValue(['first']); @@ -850,7 +807,6 @@ describe('editor_frame', () => { mount( { }); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ - dateRange, state: datasourceState, layerId: 'first', }); @@ -1460,9 +1415,10 @@ describe('editor_frame', () => { }) ); mockDatasource.getLayers.mockReturnValue(['first']); - mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], - }); + mockDatasource.getPersistableState = jest.fn((x) => ({ + state: x, + savedObjectReferences: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + })); mockVisualization.initialize.mockReturnValue({ initialState: true }); await act(async () => { @@ -1487,14 +1443,20 @@ describe('editor_frame', () => { expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenNthCalledWith(1, { - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], + filterableIndexPatterns: ['1'], doc: { - expression: '', id: undefined, + description: undefined, + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], state: { visualization: null, // Not yet loaded - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'resolved' }] }, - datasourceStates: { testDatasource: undefined }, + datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1502,18 +1464,23 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: false, }); expect(onChange).toHaveBeenLastCalledWith({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], + filterableIndexPatterns: ['1'], doc: { - expression: '', + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], + description: undefined, id: undefined, state: { visualization: { initialState: true }, // Now loaded - datasourceMetaData: { - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], - }, - datasourceStates: { testDatasource: undefined }, + datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1521,6 +1488,7 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: false, }); }); @@ -1562,11 +1530,10 @@ describe('editor_frame', () => { expect(onChange).toHaveBeenNthCalledWith(3, { filterableIndexPatterns: [], doc: { - expression: expect.stringContaining('vis "expression"'), id: undefined, + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, - datasourceStates: { testDatasource: undefined }, + datasourceStates: { testDatasource: { datasource: '' } }, visualization: { initialState: true }, query: { query: 'new query', language: 'lucene' }, filters: [], @@ -1575,6 +1542,7 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: true, }); }); @@ -1583,9 +1551,10 @@ describe('editor_frame', () => { mockDatasource.initialize.mockResolvedValue({}); mockDatasource.getLayers.mockReturnValue(['first']); - mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], - }); + mockDatasource.getPersistableState = jest.fn((x) => ({ + state: x, + savedObjectReferences: [{ type: 'index-pattern', id: '1', name: '' }], + })); mockVisualization.initialize.mockReturnValue({ initialState: true }); await act(async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 48a3511a8f359..72ad8e074226c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -7,13 +7,7 @@ import React, { useEffect, useReducer } from 'react'; import { CoreSetup, CoreStart } from 'kibana/public'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; -import { - Datasource, - DatasourcePublicAPI, - FramePublicAPI, - Visualization, - DatasourceMetaData, -} from '../../types'; +import { Datasource, FramePublicAPI, Visualization } from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; import { ConfigPanelWrapper } from './config_panel'; @@ -26,6 +20,7 @@ import { getSavedObjectFormat } from './save'; import { generateId } from '../../id_generator'; import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; import { EditorFrameStartPlugins } from '../service'; +import { initializeDatasources, createDatasourceLayers } from './state_helpers'; export interface EditorFrameProps { doc?: Document; @@ -45,8 +40,9 @@ export interface EditorFrameProps { filters: Filter[]; savedQuery?: SavedQuery; onChange: (arg: { - filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + filterableIndexPatterns: string[]; doc: Document; + isSaveable: boolean; }) => void; showNoDataPopover: () => void; } @@ -67,25 +63,19 @@ export function EditorFrame(props: EditorFrameProps) { // prevents executing dispatch on unmounted component let isUnmounted = false; if (!allLoaded) { - Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => { - if ( - state.datasourceStates[datasourceId] && - state.datasourceStates[datasourceId].isLoading - ) { - datasource - .initialize(state.datasourceStates[datasourceId].state || undefined) - .then((datasourceState) => { - if (!isUnmounted) { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: datasourceState, - datasourceId, - }); - } - }) - .catch(onError); - } - }); + initializeDatasources(props.datasourceMap, state.datasourceStates, props.doc?.references) + .then((result) => { + if (!isUnmounted) { + Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: datasourceState, + datasourceId, + }); + }); + } + }) + .catch(onError); } return () => { isUnmounted = true; @@ -95,22 +85,7 @@ export function EditorFrame(props: EditorFrameProps) { [allLoaded, onError] ); - const datasourceLayers: Record = {}; - Object.keys(props.datasourceMap) - .filter((id) => state.datasourceStates[id] && !state.datasourceStates[id].isLoading) - .forEach((id) => { - const datasourceState = state.datasourceStates[id].state; - const datasource = props.datasourceMap[id]; - - const layers = datasource.getLayers(datasourceState); - layers.forEach((layer) => { - datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({ - state: datasourceState, - layerId: layer, - dateRange: props.dateRange, - }); - }); - }); + const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates); const framePublicAPI: FramePublicAPI = { datasourceLayers, @@ -165,7 +140,18 @@ export function EditorFrame(props: EditorFrameProps) { if (props.doc) { dispatch({ type: 'VISUALIZATION_LOADED', - doc: props.doc, + doc: { + ...props.doc, + state: { + ...props.doc.state, + visualization: props.doc.visualizationType + ? props.visualizationMap[props.doc.visualizationType].initialize( + framePublicAPI, + props.doc.state.visualization + ) + : props.doc.state.visualization, + }, + }, }); } else { dispatch({ @@ -206,36 +192,20 @@ export function EditorFrame(props: EditorFrameProps) { return; } - const indexPatterns: DatasourceMetaData['filterableIndexPatterns'] = []; - Object.entries(props.datasourceMap) - .filter(([id, datasource]) => { - const stateWrapper = state.datasourceStates[id]; - return ( - stateWrapper && - !stateWrapper.isLoading && - datasource.getLayers(stateWrapper.state).length > 0 - ); + props.onChange( + getSavedObjectFormat({ + activeDatasources: Object.keys(state.datasourceStates).reduce( + (datasourceMap, datasourceId) => ({ + ...datasourceMap, + [datasourceId]: props.datasourceMap[datasourceId], + }), + {} + ), + visualization: activeVisualization, + state, + framePublicAPI, }) - .forEach(([id, datasource]) => { - indexPatterns.push( - ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns - ); - }); - - const doc = getSavedObjectFormat({ - activeDatasources: Object.keys(state.datasourceStates).reduce( - (datasourceMap, datasourceId) => ({ - ...datasourceMap, - [datasourceId]: props.datasourceMap[datasourceId], - }), - {} - ), - visualization: activeVisualization, - state, - framePublicAPI, - }); - - props.onChange({ filterableIndexPatterns: indexPatterns, doc }); + ); }, // eslint-disable-next-line react-hooks/exhaustive-deps [ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index ee28ccfe1bf53..952718e13c8cf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -5,8 +5,7 @@ */ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { Visualization, Datasource, FramePublicAPI } from '../../types'; -import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public'; +import { Visualization, Datasource, DatasourcePublicAPI } from '../../types'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, @@ -58,40 +57,12 @@ export function prependDatasourceExpression( ? fromExpression(visualizationExpression) : visualizationExpression; - return { - type: 'expression', - chain: [datafetchExpression, ...parsedVisualizationExpression.chain], - }; -} - -export function prependKibanaContext( - expression: Ast | string, - { - timeRange, - query, - filters, - }: { - timeRange?: TimeRange; - query?: Query; - filters?: Filter[]; - } -): Ast { - const parsedExpression = typeof expression === 'string' ? fromExpression(expression) : expression; - return { type: 'expression', chain: [ { type: 'function', function: 'kibana', arguments: {} }, - { - type: 'function', - function: 'kibana_context', - arguments: { - timeRange: timeRange ? [JSON.stringify(timeRange)] : [], - query: query ? [JSON.stringify(query)] : [], - filters: [JSON.stringify(filters || [])], - }, - }, - ...parsedExpression.chain, + datafetchExpression, + ...parsedVisualizationExpression.chain, ], }; } @@ -101,8 +72,7 @@ export function buildExpression({ visualizationState, datasourceMap, datasourceStates, - framePublicAPI, - removeDateRange, + datasourceLayers, }: { visualization: Visualization | null; visualizationState: unknown; @@ -114,24 +84,12 @@ export function buildExpression({ state: unknown; } >; - framePublicAPI: FramePublicAPI; - removeDateRange?: boolean; + datasourceLayers: Record; }): Ast | null { if (visualization === null) { return null; } - const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI); - - const expressionContext = removeDateRange - ? { query: framePublicAPI.query, filters: framePublicAPI.filters } - : { - query: framePublicAPI.query, - timeRange: { - from: framePublicAPI.dateRange.fromDate, - to: framePublicAPI.dateRange.toDate, - }, - filters: framePublicAPI.filters, - }; + const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers); const completeExpression = prependDatasourceExpression( visualizationExpression, @@ -139,9 +97,5 @@ export function buildExpression({ datasourceStates ); - if (completeExpression) { - return prependKibanaContext(completeExpression, expressionContext); - } else { - return null; - } + return completeExpression; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts index d72e5c57ce56e..45d24fd30e2fc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts @@ -8,14 +8,18 @@ import { getSavedObjectFormat, Props } from './save'; import { createMockDatasource, createMockVisualization } from '../mocks'; import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public'; +jest.mock('./expression_helpers'); + describe('save editor frame state', () => { const mockVisualization = createMockVisualization(); - mockVisualization.getPersistableState.mockImplementation((x) => x); const mockDatasource = createMockDatasource('a'); const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern; const mockField = ({ name: '@timestamp' } as unknown) as IFieldType; - mockDatasource.getPersistableState.mockImplementation((x) => x); + mockDatasource.getPersistableState.mockImplementation((x) => ({ + state: x, + savedObjectReferences: [], + })); const saveArgs: Props = { activeDatasources: { indexpattern: mockDatasource, @@ -47,15 +51,17 @@ describe('save editor frame state', () => { it('transforms from internal state to persisted doc format', async () => { const datasource = createMockDatasource('a'); datasource.getPersistableState.mockImplementation((state) => ({ - stuff: `${state}_datasource_persisted`, + state: { + stuff: `${state}_datasource_persisted`, + }, + savedObjectReferences: [], })); + datasource.toExpression.mockReturnValue('my | expr'); const visualization = createMockVisualization(); - visualization.getPersistableState.mockImplementation((state) => ({ - things: `${state}_vis_persisted`, - })); + visualization.toExpression.mockReturnValue('vis | expr'); - const doc = await getSavedObjectFormat({ + const { doc, filterableIndexPatterns, isSaveable } = await getSavedObjectFormat({ ...saveArgs, activeDatasources: { indexpattern: datasource, @@ -74,27 +80,32 @@ describe('save editor frame state', () => { visualization, }); + expect(filterableIndexPatterns).toEqual([]); + expect(isSaveable).toEqual(true); expect(doc).toEqual({ id: undefined, - expression: '', state: { - datasourceMetaData: { - filterableIndexPatterns: [], - }, datasourceStates: { indexpattern: { stuff: '2_datasource_persisted', }, }, - visualization: { things: '4_vis_persisted' }, + visualization: '4', query: { query: '', language: 'lucene' }, filters: [ { - meta: { index: 'indexpattern' }, + meta: { indexRefName: 'filter-index-pattern-0' }, exists: { field: '@timestamp' }, }, ], }, + references: [ + { + id: 'indexpattern', + name: 'filter-index-pattern-0', + type: 'index-pattern', + }, + ], title: 'bbb', type: 'lens', visualizationType: '3', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index b41e93def966e..6da6d5a8c118f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -5,11 +5,12 @@ */ import _ from 'lodash'; -import { toExpression } from '@kbn/interpreter/target/common'; +import { SavedObjectReference } from 'kibana/public'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; -import { buildExpression } from './expression_helpers'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; +import { extractFilterReferences } from '../../persistence'; +import { buildExpression } from './expression_helpers'; export interface Props { activeDatasources: Record; @@ -23,43 +24,55 @@ export function getSavedObjectFormat({ state, visualization, framePublicAPI, -}: Props): Document { +}: Props): { + doc: Document; + filterableIndexPatterns: string[]; + isSaveable: boolean; +} { + const datasourceStates: Record = {}; + const references: SavedObjectReference[] = []; + Object.entries(activeDatasources).forEach(([id, datasource]) => { + const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( + state.datasourceStates[id].state + ); + datasourceStates[id] = persistableState; + references.push(...savedObjectReferences); + }); + + const uniqueFilterableIndexPatternIds = _.uniq( + references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ); + + const { persistableFilters, references: filterReferences } = extractFilterReferences( + framePublicAPI.filters + ); + + references.push(...filterReferences); + const expression = buildExpression({ visualization, visualizationState: state.visualization.state, datasourceMap: activeDatasources, datasourceStates: state.datasourceStates, - framePublicAPI, - removeDateRange: true, - }); - - const datasourceStates: Record = {}; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - datasourceStates[id] = datasource.getPersistableState(state.datasourceStates[id].state); - }); - - const filterableIndexPatterns: Array<{ id: string; title: string }> = []; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - filterableIndexPatterns.push( - ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns - ); + datasourceLayers: framePublicAPI.datasourceLayers, }); return { - id: state.persistedId, - title: state.title, - description: state.description, - type: 'lens', - visualizationType: state.visualization.activeId, - expression: expression ? toExpression(expression) : '', - state: { - datasourceStates, - datasourceMetaData: { - filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'), + doc: { + id: state.persistedId, + title: state.title, + description: state.description, + type: 'lens', + visualizationType: state.visualization.activeId, + state: { + datasourceStates, + visualization: state.visualization.state, + query: framePublicAPI.query, + filters: persistableFilters, }, - visualization: visualization.getPersistableState(state.visualization.state), - query: framePublicAPI.query, - filters: framePublicAPI.filters, + references, }, + filterableIndexPatterns: uniqueFilterableIndexPatternIds, + isSaveable: expression !== null, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts new file mode 100644 index 0000000000000..6deb9ffd37a06 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -0,0 +1,87 @@ +/* + * 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 { SavedObjectReference } from 'kibana/public'; +import { Ast } from '@kbn/interpreter/common'; +import { Datasource, DatasourcePublicAPI, Visualization } from '../../types'; +import { buildExpression } from './expression_helpers'; +import { Document } from '../../persistence/saved_object_store'; + +export async function initializeDatasources( + datasourceMap: Record, + datasourceStates: Record, + references?: SavedObjectReference[] +) { + const states: Record = {}; + await Promise.all( + Object.entries(datasourceMap).map(([datasourceId, datasource]) => { + if (datasourceStates[datasourceId]) { + return datasource + .initialize(datasourceStates[datasourceId].state || undefined, references) + .then((datasourceState) => { + states[datasourceId] = { isLoading: false, state: datasourceState }; + }); + } + }) + ); + return states; +} + +export function createDatasourceLayers( + datasourceMap: Record, + datasourceStates: Record +) { + const datasourceLayers: Record = {}; + Object.keys(datasourceMap) + .filter((id) => datasourceStates[id] && !datasourceStates[id].isLoading) + .forEach((id) => { + const datasourceState = datasourceStates[id].state; + const datasource = datasourceMap[id]; + + const layers = datasource.getLayers(datasourceState); + layers.forEach((layer) => { + datasourceLayers[layer] = datasourceMap[id].getPublicAPI({ + state: datasourceState, + layerId: layer, + }); + }); + }); + return datasourceLayers; +} + +export async function persistedStateToExpression( + datasources: Record, + visualizations: Record, + doc: Document +): Promise { + const { + state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates }, + visualizationType, + references, + } = doc; + if (!visualizationType) return null; + const visualization = visualizations[visualizationType!]; + const datasourceStates = await initializeDatasources( + datasources, + Object.fromEntries( + Object.entries(persistedDatasourceStates).map(([id, state]) => [ + id, + { isLoading: false, state }, + ]) + ), + references + ); + + const datasourceLayers = createDatasourceLayers(datasources, datasourceStates); + + return buildExpression({ + visualization, + visualizationState, + datasourceMap: datasources, + datasourceStates, + datasourceLayers, + }); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 969467b5789ec..c7f505aeca517 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -57,19 +57,16 @@ describe('editor_frame state management', () => { const initialState = getInitialState({ ...props, doc: { - expression: '', state: { datasourceStates: { testDatasource: { internalState1: '' }, testDatasource2: { internalState2: '' }, }, visualization: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], title: '', visualizationType: 'testVis', }, @@ -380,9 +377,7 @@ describe('editor_frame state management', () => { type: 'VISUALIZATION_LOADED', doc: { id: 'b', - expression: '', state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { a: { foo: 'c' } }, visualization: { bar: 'd' }, query: { query: '', language: 'lucene' }, @@ -392,6 +387,7 @@ describe('editor_frame state management', () => { description: 'My lens', type: 'lens', visualizationType: 'line', + references: [], }, } ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 263f7cd65f43d..2bb1baf9d54f2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -107,7 +107,7 @@ export function getSuggestions({ * title and preview expression. */ function getVisualizationSuggestions( - visualization: Visualization, + visualization: Visualization, table: TableSuggestion, visualizationId: string, datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index fd509c0046e13..323472d717352 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -249,7 +249,6 @@ describe('suggestion_panel', () => { expect(passedExpression).toMatchInlineSnapshot(` "kibana - | kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" filters=\\"[{\\\\\\"meta\\\\\\":{\\\\\\"index\\\\\\":\\\\\\"index1\\\\\\"},\\\\\\"exists\\\\\\":{\\\\\\"field\\\\\\":\\\\\\"myfield\\\\\\"}}]\\" | lens_merge_tables layerIds=\\"first\\" tables={datasource_expression} | test | expression" diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 7395075cf9f74..f1dc3fa306d15 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -21,6 +21,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon'; import { Ast, toExpression } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; import { Action, PreviewState } from './state_management'; import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; @@ -28,7 +29,7 @@ import { ReactExpressionRendererProps, ReactExpressionRendererType, } from '../../../../../../src/plugins/expressions/public'; -import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; +import { prependDatasourceExpression } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; @@ -112,7 +113,7 @@ const SuggestionPreview = ({ }: { onSelect: () => void; preview: { - expression?: Ast; + expression?: Ast | null; icon: IconType; title: string; }; @@ -215,12 +216,24 @@ export function SuggestionPanel({ visualizationMap, ]); + const context: ExecutionContextSearch = useMemo( + () => ({ + query: frame.query, + timeRange: { + from: frame.dateRange.fromDate, + to: frame.dateRange.toDate, + }, + filters: frame.filters, + }), + [frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters] + ); + const AutoRefreshExpressionRenderer = useMemo(() => { const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$(); return (props: ReactExpressionRendererProps) => ( - + ); - }, [plugins.data.query.timefilter.timefilter]); + }, [plugins.data.query.timefilter.timefilter, context]); const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-1); @@ -252,15 +265,6 @@ export function SuggestionPanel({ } } - const expressionContext = { - query: frame.query, - filters: frame.filters, - timeRange: { - from: frame.dateRange.fromDate, - to: frame.dateRange.toDate, - }, - }; - return (

@@ -305,9 +309,7 @@ export function SuggestionPanel({ {currentVisualizationId && ( , + newVisualization: Visualization, subVisualizationId?: string ): Suggestion | undefined { const unfilteredSuggestions = getSuggestions({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index a9c638df8cad1..47e3b41df3b21 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -172,21 +172,6 @@ describe('workspace_panel', () => { "function": "kibana", "type": "function", }, - Object { - "arguments": Object { - "filters": Array [ - "[]", - ], - "query": Array [ - "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", - ], - "timeRange": Array [ - "{\\"from\\":\\"now-7d\\",\\"to\\":\\"now\\"}", - ], - }, - "function": "kibana_context", - "type": "function", - }, Object { "arguments": Object { "layerIds": Array [ @@ -305,10 +290,10 @@ describe('workspace_panel', () => { ); expect( - (instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.layerIds + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.layerIds ).toEqual(['first', 'second', 'third']); expect( - (instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.tables + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.tables ).toMatchInlineSnapshot(` Array [ Object { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index b3a12271f377b..4f914bc65dc7c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -18,6 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { CoreStart, CoreSetup } from 'kibana/public'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -129,7 +130,7 @@ export function InnerWorkspacePanel({ visualizationState, datasourceMap, datasourceStates, - framePublicAPI, + datasourceLayers: framePublicAPI.datasourceLayers, }); } catch (e) { // Most likely an error in the expression provided by a datasource or visualization @@ -173,6 +174,23 @@ export function InnerWorkspacePanel({ [plugins.data.query.timefilter.timefilter] ); + const context: ExecutionContextSearch = useMemo( + () => ({ + query: framePublicAPI.query, + timeRange: { + from: framePublicAPI.dateRange.fromDate, + to: framePublicAPI.dateRange.toDate, + }, + filters: framePublicAPI.filters, + }), + [ + framePublicAPI.query, + framePublicAPI.dateRange.fromDate, + framePublicAPI.dateRange.toDate, + framePublicAPI.filters, + ] + ); + useEffect(() => { // reset expression error if component attempts to run it again if (expression && localState.expressionBuildError) { @@ -264,6 +282,7 @@ export function InnerWorkspacePanel({ className="lnsExpressionRenderer__component" padding="m" expression={expression!} + searchContext={context} reload$={autoRefreshFetch$} onEvent={onEvent} renderError={(errorMessage?: string | null) => { 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 69447b3b9a9b8..1e2df28cad7b1 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 @@ -18,16 +18,13 @@ jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ })); const savedVis: Document = { - expression: 'my | expression', state: { visualization: {}, datasourceStates: {}, - datasourceMetaData: { - filterableIndexPatterns: [], - }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], title: 'My title', visualizationType: '', }; @@ -59,13 +56,14 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); embeddable.render(mountpoint); expect(expressionRenderer).toHaveBeenCalledTimes(1); - expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(savedVis.expression); + expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual('my | expression'); }); it('should re-render if new input is pushed', () => { @@ -82,6 +80,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); @@ -110,6 +109,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); @@ -117,11 +117,52 @@ describe('embeddable', () => { expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ timeRange, - query, + query: [query, savedVis.state.query], filters, }); }); + it('should merge external context with query and filters of the saved object', () => { + const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const query: Query = { language: 'kquery', query: 'external filter' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; + + const embeddable = new Embeddable( + dataPluginMock.createSetupContract().query.timefilter.timefilter, + expressionRenderer, + getTrigger, + { + editPath: '', + editUrl: '', + editable: true, + savedVis: { + ...savedVis, + state: { + ...savedVis.state, + query: { language: 'kquery', query: 'saved filter' }, + filters: [ + { meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } }, + ], + }, + references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }], + }, + expression: 'my | expression', + }, + { id: '123', timeRange, query, filters } + ); + embeddable.render(mountpoint); + + expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ + timeRange, + query: [query, { language: 'kquery', query: 'saved filter' }], + filters: [ + filters[0], + // actual index pattern id gets injected + { meta: { alias: 'test', negate: false, disabled: false, index: 'my-index-pattern-id' } }, + ], + }); + }); + it('should execute trigger on event from expression renderer', () => { const embeddable = new Embeddable( dataPluginMock.createSetupContract().query.timefilter.timefilter, @@ -132,6 +173,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); @@ -162,6 +204,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); @@ -195,6 +238,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index bbd2b18907e9b..4df218a3e94e9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -14,6 +14,7 @@ import { TimefilterContract, TimeRange, } from 'src/plugins/data/public'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; import { Subscription } from 'rxjs'; import { @@ -28,12 +29,13 @@ import { EmbeddableOutput, IContainer, } from '../../../../../../src/plugins/embeddable/public'; -import { DOC_TYPE, Document } from '../../persistence'; +import { DOC_TYPE, Document, injectFilterReferences } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, isLensFilterEvent } from '../../types'; export interface LensEmbeddableConfiguration { + expression: string | null; savedVis: Document; editUrl: string; editPath: string; @@ -56,12 +58,13 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(input)); this.onContainerStateChanged(initialInput); @@ -122,14 +133,14 @@ export class Embeddable extends AbstractEmbeddable !filter.meta.disabled) : undefined; if ( - !_.isEqual(containerState.timeRange, this.currentContext.timeRange) || - !_.isEqual(containerState.query, this.currentContext.query) || - !_.isEqual(cleanedFilters, this.currentContext.filters) + !_.isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || + !_.isEqual(containerState.query, this.externalSearchContext.query) || + !_.isEqual(cleanedFilters, this.externalSearchContext.filters) ) { - this.currentContext = { + this.externalSearchContext = { timeRange: containerState.timeRange, query: containerState.query, - lastReloadRequestTime: this.currentContext.lastReloadRequestTime, + lastReloadRequestTime: this.externalSearchContext.lastReloadRequestTime, filters: cleanedFilters, }; @@ -149,14 +160,37 @@ export class Embeddable extends AbstractEmbeddable, domNode ); } + /** + * Combines the embeddable context with the saved object context, and replaces + * any references to index patterns + */ + private getMergedSearchContext(): ExecutionContextSearch { + const output: ExecutionContextSearch = { + timeRange: this.externalSearchContext.timeRange, + }; + if (this.externalSearchContext.query) { + output.query = [this.externalSearchContext.query, this.savedVis.state.query]; + } else { + output.query = [this.savedVis.state.query]; + } + if (this.externalSearchContext.filters?.length) { + output.filters = [...this.externalSearchContext.filters, ...this.savedVis.state.filters]; + } else { + output.filters = [...this.savedVis.state.filters]; + } + + output.filters = injectFilterReferences(output.filters, this.savedVis.references); + return output; + } + handleEvent = (event: ExpressionRendererEvent) => { if (!this.getTrigger || this.input.disableTriggers) { return; @@ -188,9 +222,9 @@ export class Embeddable extends AbstractEmbeddable Promise; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { @@ -72,13 +75,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, timefilter, expressionRenderer, + documentToExpression, uiActions, } = await this.getStartServices(); const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); - const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( - async ({ id }) => { + const promises = savedVis.references + .filter(({ type }) => type === 'index-pattern') + .map(async ({ id }) => { try { return await indexPatternService.get(id); } catch (error) { @@ -87,14 +92,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { // to show. return null; } - } - ); + }); const indexPatterns = ( await Promise.all(promises) ).filter((indexPattern: IndexPattern | null): indexPattern is IndexPattern => Boolean(indexPattern) ); + const expression = await documentToExpression(savedVis); + return new Embeddable( timefilter, expressionRenderer, @@ -105,6 +111,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { editUrl: coreHttp.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`), editable: await this.isEditable(), indexPatterns, + expression: expression ? toExpression(expression) : null, }, input, parent diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index 296dcef3e70b9..d0d2360ddc107 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -8,28 +8,23 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; -import { TimeRange, Filter, Query } from 'src/plugins/data/public'; import { ExpressionRendererEvent, ReactExpressionRendererType, } from 'src/plugins/expressions/public'; +import { ExecutionContextSearch } from 'src/plugins/expressions'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; expression: string | null; - context: { - timeRange?: TimeRange; - query?: Query; - filters?: Filter[]; - lastReloadRequestTime?: number; - }; + searchContext: ExecutionContextSearch; handleEvent: (event: ExpressionRendererEvent) => void; } export function ExpressionWrapper({ ExpressionRenderer: ExpressionRendererComponent, expression, - context, + searchContext, handleEvent, }: ExpressionWrapperProps) { return ( @@ -54,7 +49,7 @@ export function ExpressionWrapper({ className="lnsExpressionRenderer__component" padding="m" expression={expression} - searchContext={{ ...context }} + searchContext={searchContext} renderError={(error) =>
{error}
} onEvent={handleEvent} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 9c0825b3c2d27..86b137851d9bd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -31,7 +31,6 @@ export function createMockVisualization(): jest.Mocked { getVisualizationTypeId: jest.fn((_state) => 'empty'), getDescription: jest.fn((_state) => ({ label: '' })), switchVisualizationType: jest.fn((_, x) => x), - getPersistableState: jest.fn((_state) => _state), getSuggestions: jest.fn((_options) => []), initialize: jest.fn((_frame, _state?) => ({})), getConfiguration: jest.fn((props) => ({ @@ -71,7 +70,7 @@ export function createMockDatasource(id: string): DatasourceMock { clearLayer: jest.fn((state, _layerId) => state), getDatasourceSuggestionsForField: jest.fn((_state, _item) => []), getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), - getPersistableState: jest.fn(), + getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [] })), getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), @@ -81,7 +80,6 @@ export function createMockDatasource(id: string): DatasourceMock { removeLayer: jest.fn((_state, _layerId) => {}), removeColumn: jest.fn((props) => {}), getLayers: jest.fn((_state) => []), - getMetaData: jest.fn((_state) => ({ filterableIndexPatterns: [] })), renderDimensionTrigger: jest.fn(), renderDimensionEditor: jest.fn(), diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 47339373b6d1a..5fc347179a032 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -21,12 +21,14 @@ import { EditorFrameInstance, EditorFrameStart, } from '../types'; +import { Document } from '../persistence/saved_object_store'; import { EditorFrame } from './editor_frame'; import { mergeTables } from './merge_tables'; import { formatColumn } from './format_column'; import { EmbeddableFactory } from './embeddable/embeddable_factory'; import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { persistedStateToExpression } from './editor_frame/state_helpers'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; @@ -59,6 +61,21 @@ export class EditorFrameService { private readonly datasources: Array> = []; private readonly visualizations: Array> = []; + /** + * This method takes a Lens saved object as returned from the persistence helper, + * initializes datsources and visualization and creates the current expression. + * This is an asynchronous process and should only be triggered once for a saved object. + * @param doc parsed Lens saved object + */ + private async documentToExpression(doc: Document) { + const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ + collectAsyncDefinitions(this.datasources), + collectAsyncDefinitions(this.visualizations), + ]); + + return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc); + } + public setup( core: CoreSetup, plugins: EditorFrameSetupPlugins @@ -74,6 +91,7 @@ export class EditorFrameService { coreHttp: coreStart.http, timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, + documentToExpression: this.documentToExpression.bind(this), indexPatternService: deps.data.indexPatterns, uiActions: deps.uiActions, }; @@ -88,7 +106,7 @@ export class EditorFrameService { this.datasources.push(datasource as Datasource); }, registerVisualization: (visualization) => { - this.visualizations.push(visualization as Visualization); + this.visualizations.push(visualization as Visualization); }, }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts index ca5fe706985f8..c487e31f5a973 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts @@ -23,3 +23,7 @@ export function loadInitialState() { }; return result; } + +const originalLoader = jest.requireActual('../loader'); + +export const extractReferences = originalLoader.extractReferences; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index dc3938ce436e5..0ba7b7df97853 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -128,12 +128,15 @@ const expectedIndexPatterns = { }, }; -function stateFromPersistedState( - persistedState: IndexPatternPersistedState -): IndexPatternPrivateState { +type IndexPatternBaseState = Omit< + IndexPatternPrivateState, + 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' +>; + +function enrichBaseState(baseState: IndexPatternBaseState): IndexPatternPrivateState { return { - currentIndexPatternId: persistedState.currentIndexPatternId, - layers: persistedState.layers, + currentIndexPatternId: baseState.currentIndexPatternId, + layers: baseState.layers, indexPatterns: expectedIndexPatterns, indexPatternRefs: [], existingFields: {}, @@ -142,7 +145,10 @@ function stateFromPersistedState( } describe('IndexPattern Data Source', () => { - let persistedState: IndexPatternPersistedState; + let baseState: Omit< + IndexPatternPrivateState, + 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' + >; let indexPatternDatasource: Datasource; beforeEach(() => { @@ -153,7 +159,7 @@ describe('IndexPattern Data Source', () => { charts: chartPluginMock.createSetupContract(), }); - persistedState = { + baseState = { currentIndexPatternId: '1', layers: { first: { @@ -224,9 +230,37 @@ describe('IndexPattern Data Source', () => { describe('#getPersistedState', () => { it('should persist from saved state', async () => { - const state = stateFromPersistedState(persistedState); + const state = enrichBaseState(baseState); - expect(indexPatternDatasource.getPersistableState(state)).toEqual(persistedState); + expect(indexPatternDatasource.getPersistableState(state)).toEqual({ + state: { + layers: { + first: { + columnOrder: ['col1'], + columns: { + col1: { + label: 'My Op', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'op', + params: { + size: 5, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + }, + }, + }, + }, + }, + savedObjectReferences: [ + { name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', id: '1' }, + { name: 'indexpattern-datasource-layer-first', type: 'index-pattern', id: '1' }, + ], + }); }); }); @@ -237,7 +271,7 @@ describe('IndexPattern Data Source', () => { }); it('should generate an expression for an aggregated query', async () => { - const queryPersistedState: IndexPatternPersistedState = { + const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', layers: { first: { @@ -266,7 +300,7 @@ describe('IndexPattern Data Source', () => { }, }; - const state = stateFromPersistedState(queryPersistedState); + const state = enrichBaseState(queryBaseState); expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(` Object { @@ -311,7 +345,7 @@ describe('IndexPattern Data Source', () => { }); it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { - const queryPersistedState: IndexPatternPersistedState = { + const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', layers: { first: { @@ -350,14 +384,14 @@ describe('IndexPattern Data Source', () => { }, }; - const state = stateFromPersistedState(queryPersistedState); + const state = enrichBaseState(queryBaseState); const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { - const queryPersistedState: IndexPatternPersistedState = { + const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', layers: { first: { @@ -386,7 +420,7 @@ describe('IndexPattern Data Source', () => { }, }; - const state = stateFromPersistedState(queryPersistedState); + const state = enrichBaseState(queryBaseState); const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']); @@ -489,55 +523,14 @@ describe('IndexPattern Data Source', () => { }); }); - describe('#getMetadata', () => { - it('should return the title of the index patterns', () => { - expect( - indexPatternDatasource.getMetaData({ - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, - layers: { - first: { - indexPatternId: '1', - columnOrder: [], - columns: {}, - }, - second: { - indexPatternId: '2', - columnOrder: [], - columns: {}, - }, - }, - currentIndexPatternId: '1', - }) - ).toEqual({ - filterableIndexPatterns: [ - { - id: '1', - title: 'my-fake-index-pattern', - }, - { - id: '2', - title: 'my-fake-restricted-pattern', - }, - ], - }); - }); - }); - describe('#getPublicAPI', () => { let publicAPI: DatasourcePublicAPI; beforeEach(async () => { - const initialState = stateFromPersistedState(persistedState); + const initialState = enrichBaseState(baseState); publicAPI = indexPatternDatasource.getPublicAPI({ state: initialState, layerId: 'first', - dateRange: { - fromDate: 'now-30d', - toDate: 'now', - }, }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2fb8d7fe0e553..e2ca933504849 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'kibana/public'; +import { CoreStart, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -19,7 +19,12 @@ import { DatasourceLayerPanelProps, PublicAPIProps, } from '../types'; -import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; +import { + loadInitialState, + changeIndexPattern, + changeLayerIndexPattern, + extractReferences, +} from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionTrigger, @@ -125,9 +130,13 @@ export function getIndexPatternDatasource({ const indexPatternDatasource: Datasource = { id: 'indexpattern', - async initialize(state?: IndexPatternPersistedState) { + async initialize( + persistedState?: IndexPatternPersistedState, + references?: SavedObjectReference[] + ) { return loadInitialState({ - state, + persistedState, + references, savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, @@ -135,8 +144,8 @@ export function getIndexPatternDatasource({ }); }, - getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) { - return { currentIndexPatternId, layers }; + getPersistableState(state: IndexPatternPrivateState) { + return extractReferences(state); }, insertLayer(state: IndexPatternPrivateState, newLayerId: string) { @@ -183,19 +192,6 @@ export function getIndexPatternDatasource({ toExpression, - getMetaData(state: IndexPatternPrivateState) { - return { - filterableIndexPatterns: _.uniq( - Object.values(state.layers) - .map((layer) => layer.indexPatternId) - .map((indexPatternId) => ({ - id: indexPatternId, - title: state.indexPatterns[indexPatternId].title, - })) - ), - }; - }, - renderDataPanel( domElement: Element, props: DatasourceDataPanelProps diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index cfabcb4edcef7..d80bf779a5d17 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -12,6 +12,8 @@ import { changeIndexPattern, changeLayerIndexPattern, syncExistingFields, + extractReferences, + injectReferences, } from './loader'; import { IndexPatternsContract } from '../../../../../src/plugins/data/public'; import { @@ -378,10 +380,8 @@ describe('loader', () => { it('should initialize from saved state', async () => { const savedState: IndexPatternPersistedState = { - currentIndexPatternId: '2', layers: { layerb: { - indexPatternId: '2', columnOrder: ['col1', 'col2'], columns: { col1: { @@ -407,7 +407,12 @@ describe('loader', () => { }; const storage = createMockStorage({ indexPatternId: '1' }); const state = await loadInitialState({ - state: savedState, + persistedState: savedState, + references: [ + { name: 'indexpattern-datasource-current-indexpattern', id: '2', type: 'index-pattern' }, + { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, + { name: 'another-reference', id: 'c', type: 'index-pattern' }, + ], savedObjectsClient: mockClient(), indexPatternsService: mockIndexPatternsService(), storage, @@ -422,7 +427,7 @@ describe('loader', () => { indexPatterns: { '2': sampleIndexPatterns['2'], }, - layers: savedState.layers, + layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -431,6 +436,79 @@ describe('loader', () => { }); }); + describe('saved object references', () => { + const state: IndexPatternPrivateState = { + currentIndexPatternId: 'b', + indexPatternRefs: [], + indexPatterns: {}, + existingFields: {}, + layers: { + a: { + indexPatternId: 'id-index-pattern-a', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'myfield', + }, + }, + }, + b: { + indexPatternId: 'id-index-pattern-b', + columnOrder: ['col2'], + columns: { + col2: { + dataType: 'number', + isBucketed: false, + label: '', + operationType: 'avg', + sourceField: 'myfield2', + }, + }, + }, + }, + isFirstExistenceFetch: false, + }; + + it('should create a reference for each layer and for current index pattern', () => { + const { savedObjectReferences } = extractReferences(state); + expect(savedObjectReferences).toMatchInlineSnapshot(` + Array [ + Object { + "id": "b", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "id-index-pattern-a", + "name": "indexpattern-datasource-layer-a", + "type": "index-pattern", + }, + Object { + "id": "id-index-pattern-b", + "name": "indexpattern-datasource-layer-b", + "type": "index-pattern", + }, + ] + `); + }); + + it('should restore layers', () => { + const { savedObjectReferences, state: persistedState } = extractReferences(state); + expect(injectReferences(persistedState, savedObjectReferences).layers).toEqual(state.layers); + }); + + it('should restore current index pattern', () => { + const { savedObjectReferences, state: persistedState } = extractReferences(state); + expect(injectReferences(persistedState, savedObjectReferences).currentIndexPatternId).toEqual( + state.currentIndexPatternId + ); + }); + }); + describe('changeIndexPattern', () => { it('loads the index pattern and then sets it as current', async () => { const setState = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 9c4a19e58a052..24906790a9fc9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public'; import { StateSetter } from '../types'; import { IndexPattern, @@ -14,6 +14,7 @@ import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField, + IndexPatternLayer, } from './types'; import { updateLayerIndexPattern } from './state_helpers'; import { DateRange, ExistingFields } from '../../common/types'; @@ -115,14 +116,58 @@ const setLastUsedIndexPatternId = (storage: IStorageWrapper, value: string) => { writeToStorage(storage, 'indexPatternId', value); }; +const CURRENT_PATTERN_REFERENCE_NAME = 'indexpattern-datasource-current-indexpattern'; +function getLayerReferenceName(layerId: string) { + return `indexpattern-datasource-layer-${layerId}`; +} + +export function extractReferences({ currentIndexPatternId, layers }: IndexPatternPrivateState) { + const savedObjectReferences: SavedObjectReference[] = []; + savedObjectReferences.push({ + type: 'index-pattern', + id: currentIndexPatternId, + name: CURRENT_PATTERN_REFERENCE_NAME, + }); + const persistableLayers: Record> = {}; + Object.entries(layers).forEach(([layerId, { indexPatternId, ...persistableLayer }]) => { + savedObjectReferences.push({ + type: 'index-pattern', + id: indexPatternId, + name: getLayerReferenceName(layerId), + }); + persistableLayers[layerId] = persistableLayer; + }); + return { savedObjectReferences, state: { layers: persistableLayers } }; +} + +export function injectReferences( + state: IndexPatternPersistedState, + references: SavedObjectReference[] +) { + const layers: Record = {}; + Object.entries(state.layers).forEach(([layerId, persistedLayer]) => { + layers[layerId] = { + ...persistedLayer, + indexPatternId: references.find(({ name }) => name === getLayerReferenceName(layerId))!.id, + }; + }); + return { + currentIndexPatternId: references.find(({ name }) => name === CURRENT_PATTERN_REFERENCE_NAME)! + .id, + layers, + }; +} + export async function loadInitialState({ - state, + persistedState, + references, savedObjectsClient, defaultIndexPatternId, storage, indexPatternsService, }: { - state?: IndexPatternPersistedState; + persistedState?: IndexPatternPersistedState; + references?: SavedObjectReference[]; savedObjectsClient: SavedObjectsClient; defaultIndexPatternId?: string; storage: IStorageWrapper; @@ -131,6 +176,9 @@ export async function loadInitialState({ const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); + const state = + persistedState && references ? injectReferences(persistedState, references) : undefined; + const requiredPatterns = _.uniq( state ? Object.values(state.layers) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 8d0e82b176aa9..95cc47e68f8a1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -40,11 +40,12 @@ export interface IndexPatternLayer { } export interface IndexPatternPersistedState { - currentIndexPatternId: string; - layers: Record; + layers: Record>; } -export type IndexPatternPrivateState = IndexPatternPersistedState & { +export interface IndexPatternPrivateState { + currentIndexPatternId: string; + layers: Record; indexPatternRefs: IndexPatternRef[]; indexPatterns: Record; @@ -54,7 +55,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { existingFields: Record>; isFirstExistenceFetch: boolean; existenceFetchFailed?: boolean; -}; +} export interface IndexPatternRef { id: string; diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts index 62f47a21c85b0..f3c9a725ee2e2 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -66,12 +66,6 @@ describe('metric_visualization', () => { }); }); - describe('#getPersistableState', () => { - it('persists the state as given', () => { - expect(metricVisualization.getPersistableState(exampleState())).toEqual(exampleState()); - }); - }); - describe('#getConfiguration', () => { it('can add a metric when there is no accessor', () => { expect( @@ -168,7 +162,8 @@ describe('metric_visualization', () => { datasourceLayers: { l1: datasource }, }; - expect(metricVisualization.toExpression(exampleState(), frame)).toMatchInlineSnapshot(` + expect(metricVisualization.toExpression(exampleState(), frame.datasourceLayers)) + .toMatchInlineSnapshot(` Object { "chain": Array [ Object { diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index e565d2fa8b293..5f1ce5334dd36 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -7,20 +7,20 @@ import { i18n } from '@kbn/i18n'; import { Ast } from '@kbn/interpreter/target/common'; import { getSuggestions } from './metric_suggestions'; -import { Visualization, FramePublicAPI, OperationMetadata } from '../types'; -import { State, PersistableState } from './types'; +import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types'; +import { State } from './types'; import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( state: State, - frame: FramePublicAPI, + datasourceLayers: Record, mode: 'reduced' | 'full' = 'full' ): Ast | null => { if (!state.accessor) { return null; } - const [datasource] = Object.values(frame.datasourceLayers); + const [datasource] = Object.values(datasourceLayers); const operation = datasource && datasource.getOperationForColumnId(state.accessor); return { @@ -39,7 +39,7 @@ const toExpression = ( }; }; -export const metricVisualization: Visualization = { +export const metricVisualization: Visualization = { id: 'lnsMetric', visualizationTypes: [ @@ -88,8 +88,6 @@ export const metricVisualization: Visualization = { ); }, - getPersistableState: (state) => state, - getConfiguration(props) { return { groups: [ @@ -106,8 +104,8 @@ export const metricVisualization: Visualization = { }, toExpression, - toPreviewExpression: (state: State, frame: FramePublicAPI) => - toExpression(state, frame, 'reduced'), + toPreviewExpression: (state, datasourceLayers) => + toExpression(state, datasourceLayers, 'reduced'), setDimension({ prevState, columnId }) { return { ...prevState, accessor: columnId }; diff --git a/x-pack/plugins/lens/public/metric_visualization/types.ts b/x-pack/plugins/lens/public/metric_visualization/types.ts index 53fc103934255..86a781716b345 100644 --- a/x-pack/plugins/lens/public/metric_visualization/types.ts +++ b/x-pack/plugins/lens/public/metric_visualization/types.ts @@ -13,5 +13,3 @@ export interface MetricConfig extends State { title: string; mode: 'reduced' | 'full'; } - -export type PersistableState = State; diff --git a/x-pack/plugins/lens/public/persistence/filter_references.test.ts b/x-pack/plugins/lens/public/persistence/filter_references.test.ts new file mode 100644 index 0000000000000..23c0cd1d11f1b --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/filter_references.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter } from 'src/plugins/data/public'; +import { extractFilterReferences, injectFilterReferences } from './filter_references'; +import { FilterStateStore } from 'src/plugins/data/common'; + +describe('filter saved object references', () => { + const filters: Filter[] = [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.src', + negate: true, + params: { query: 'CN' }, + type: 'phrase', + }, + query: { match_phrase: { 'geo.src': 'CN' } }, + }, + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + key: 'geoip.country_iso_code', + negate: true, + params: { query: 'US' }, + type: 'phrase', + }, + query: { match_phrase: { 'geoip.country_iso_code': 'US' } }, + }, + ]; + + it('should create two index-pattern references', () => { + const { references } = extractFilterReferences(filters); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "filter-index-pattern-0", + "type": "index-pattern", + }, + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "filter-index-pattern-1", + "type": "index-pattern", + }, + ] + `); + }); + + it('should restore the same filter after extracting and injecting', () => { + const { persistableFilters, references } = extractFilterReferences(filters); + expect(injectFilterReferences(persistableFilters, references)).toEqual(filters); + }); + + it('should ignore other references', () => { + const { persistableFilters, references } = extractFilterReferences(filters); + expect( + injectFilterReferences(persistableFilters, [ + { type: 'index-pattern', id: '1234', name: 'some other index pattern' }, + ...references, + ]) + ).toEqual(filters); + }); + + it('should inject other ids if references change', () => { + const { persistableFilters, references } = extractFilterReferences(filters); + + expect( + injectFilterReferences( + persistableFilters, + references.map((reference, index) => ({ ...reference, id: `overwritten-id-${index}` })) + ) + ).toEqual([ + { + ...filters[0], + meta: { + ...filters[0].meta, + index: 'overwritten-id-0', + }, + }, + { + ...filters[1], + meta: { + ...filters[1].meta, + index: 'overwritten-id-1', + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/lens/public/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts new file mode 100644 index 0000000000000..47564e510ce9c --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter } from 'src/plugins/data/public'; +import { SavedObjectReference } from 'kibana/public'; +import { PersistableFilter } from '../../common'; + +export function extractFilterReferences( + filters: Filter[] +): { persistableFilters: PersistableFilter[]; references: SavedObjectReference[] } { + const references: SavedObjectReference[] = []; + const persistableFilters = filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `filter-index-pattern-${i}`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }); + + return { persistableFilters, references }; +} + +export function injectFilterReferences( + filters: PersistableFilter[], + references: SavedObjectReference[] +) { + return filters.map((filterRow) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return filterRow as Filter; + } + const { indexRefName, ...metaRest } = filterRow.meta; + const reference = references.find((ref) => ref.name === indexRefName); + if (!reference) { + throw new Error(`Could not find reference for ${indexRefName}`); + } + return { + ...filterRow, + meta: { ...metaRest, index: reference.id }, + }; + }); +} diff --git a/x-pack/plugins/lens/public/persistence/index.ts b/x-pack/plugins/lens/public/persistence/index.ts index 1f823ff75c8c6..464bd46790422 100644 --- a/x-pack/plugins/lens/public/persistence/index.ts +++ b/x-pack/plugins/lens/public/persistence/index.ts @@ -5,3 +5,4 @@ */ export * from './saved_object_store'; +export * from './filter_references'; diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts index f8f8d889233a7..ba7c0ee6ae786 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts @@ -30,11 +30,8 @@ describe('LensStore', () => { title: 'Hello', description: 'My doc', visualizationType: 'bar', - expression: '', + references: [], state: { - datasourceMetaData: { - filterableIndexPatterns: [], - }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, @@ -49,11 +46,8 @@ describe('LensStore', () => { title: 'Hello', description: 'My doc', visualizationType: 'bar', - expression: '', + references: [], state: { - datasourceMetaData: { - filterableIndexPatterns: [], - }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, @@ -64,21 +58,25 @@ describe('LensStore', () => { }); expect(client.create).toHaveBeenCalledTimes(1); - expect(client.create).toHaveBeenCalledWith('lens', { - title: 'Hello', - description: 'My doc', - visualizationType: 'bar', - expression: '', - state: { - datasourceMetaData: { filterableIndexPatterns: [] }, - datasourceStates: { - indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + expect(client.create).toHaveBeenCalledWith( + 'lens', + { + title: 'Hello', + description: 'My doc', + visualizationType: 'bar', + state: { + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + }, + visualization: { x: 'foo', y: 'baz' }, + query: { query: '', language: 'lucene' }, + filters: [], }, - visualization: { x: 'foo', y: 'baz' }, - query: { query: '', language: 'lucene' }, - filters: [], }, - }); + { + references: [], + } + ); }); test('updates and returns a visualization document', async () => { @@ -87,9 +85,8 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, @@ -101,9 +98,8 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, @@ -116,22 +112,21 @@ describe('LensStore', () => { { type: 'lens', id: 'Gandalf', + references: [], attributes: { title: null, visualizationType: null, - expression: null, state: null, }, }, { type: 'lens', id: 'Gandalf', + references: [], attributes: { title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', state: { - datasourceMetaData: { filterableIndexPatterns: [] }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index 59ead53956a8d..e4609213ec792 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/public'; -import { Query, Filter } from '../../../../../src/plugins/data/public'; +import { + SavedObjectAttributes, + SavedObjectsClientContract, + SavedObjectReference, +} from 'kibana/public'; +import { Query } from '../../../../../src/plugins/data/public'; +import { PersistableFilter } from '../../common'; export interface Document { id?: string; @@ -13,16 +18,13 @@ export interface Document { visualizationType: string | null; title: string; description?: string; - expression: string | null; state: { - datasourceMetaData: { - filterableIndexPatterns: Array<{ id: string; title: string }>; - }; datasourceStates: Record; visualization: unknown; query: Query; - filters: Filter[]; + filters: PersistableFilter[]; }; + references: SavedObjectReference[]; } export const DOC_TYPE = 'lens'; @@ -45,14 +47,16 @@ export class SavedObjectIndexStore implements SavedObjectStore { } async save(vis: Document) { - const { id, type, ...rest } = vis; + const { id, type, references, ...rest } = vis; // TODO: SavedObjectAttributes should support this kind of object, // remove this workaround when SavedObjectAttributes is updated. const attributes = (rest as unknown) as SavedObjectAttributes; const result = await (id - ? this.safeUpdate(id, attributes) - : this.client.create(DOC_TYPE, attributes)); + ? this.safeUpdate(id, attributes, references) + : this.client.create(DOC_TYPE, attributes, { + references, + })); return { ...vis, id: result.id }; } @@ -63,21 +67,25 @@ export class SavedObjectIndexStore implements SavedObjectStore { // deleted subtrees make it back into the object after a load. // This function fixes this by doing two updates - one to empty out the document setting // every key to null, and a second one to load the new content. - private async safeUpdate(id: string, attributes: SavedObjectAttributes) { + private async safeUpdate( + id: string, + attributes: SavedObjectAttributes, + references: SavedObjectReference[] + ) { const resetAttributes: SavedObjectAttributes = {}; Object.keys(attributes).forEach((key) => { resetAttributes[key] = null; }); return ( await this.client.bulkUpdate([ - { type: DOC_TYPE, id, attributes: resetAttributes }, - { type: DOC_TYPE, id, attributes }, + { type: DOC_TYPE, id, attributes: resetAttributes, references }, + { type: DOC_TYPE, id, attributes, references }, ]) ).savedObjects[1]; } async load(id: string): Promise { - const { type, attributes, error } = await this.client.get(DOC_TYPE, id); + const { type, attributes, references, error } = await this.client.get(DOC_TYPE, id); if (error) { throw error; @@ -85,6 +93,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { return { ...(attributes as SavedObjectAttributes), + references, id, type, } as Document; diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx index 5a68516db6aa3..855bacd4f794c 100644 --- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx @@ -31,7 +31,7 @@ const bucketedOperations = (op: OperationMetadata) => op.isBucketed; const numberMetricOperations = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; -export const pieVisualization: Visualization = { +export const pieVisualization: Visualization = { id: 'lnsPie', visualizationTypes: [ @@ -91,8 +91,6 @@ export const pieVisualization: Visualization state, - getSuggestions: suggestions, getConfiguration({ state, frame, layerId }) { diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index fbc47e8bfb00f..f36b9efb930a9 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -5,21 +5,24 @@ */ import { Ast } from '@kbn/interpreter/common'; -import { FramePublicAPI, Operation } from '../types'; +import { Operation, DatasourcePublicAPI } from '../types'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { PieVisualizationState } from './types'; -export function toExpression(state: PieVisualizationState, frame: FramePublicAPI) { - return expressionHelper(state, frame, false); +export function toExpression( + state: PieVisualizationState, + datasourceLayers: Record +) { + return expressionHelper(state, datasourceLayers, false); } function expressionHelper( state: PieVisualizationState, - frame: FramePublicAPI, + datasourceLayers: Record, isPreview: boolean ): Ast | null { const layer = state.layers[0]; - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; const operations = layer.groups .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); @@ -50,6 +53,9 @@ function expressionHelper( }; } -export function toPreviewExpression(state: PieVisualizationState, frame: FramePublicAPI) { - return expressionHelper(state, frame, true); +export function toPreviewExpression( + state: PieVisualizationState, + datasourceLayers: Record +) { + return expressionHelper(state, datasourceLayers, true); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index c7bda65cd1327..20f2ce6c56774 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -7,6 +7,7 @@ import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'kibana/public'; +import { SavedObjectReference } from 'kibana/public'; import { ExpressionRendererEvent, IInterpreterRenderHandlers, @@ -30,7 +31,6 @@ export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; export interface PublicAPIProps { state: T; layerId: string; - dateRange: DateRange; } export interface EditorFrameProps { @@ -44,8 +44,9 @@ export interface EditorFrameProps { // Frame loader (app or embeddable) is expected to call this when it loads and updates // This should be replaced with a top-down state onChange: (newState: { - filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + filterableIndexPatterns: string[]; doc: Document; + isSaveable: boolean; }) => void; showNoDataPopover: () => void; } @@ -57,9 +58,7 @@ export interface EditorFrameInstance { export interface EditorFrameSetup { // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation registerDatasource: (datasource: Datasource | Promise>) => void; - registerVisualization: ( - visualization: Visualization | Promise> - ) => void; + registerVisualization: (visualization: Visualization | Promise>) => void; } export interface EditorFrameStart { @@ -131,10 +130,6 @@ export interface DatasourceSuggestion { keptLayerIds: string[]; } -export interface DatasourceMetaData { - filterableIndexPatterns: Array<{ id: string; title: string }>; -} - export type StateSetter = (newState: T | ((prevState: T) => T)) => void; /** @@ -146,10 +141,10 @@ export interface Datasource { // For initializing, either from an empty state or from persisted state // Because this will be called at runtime, state might have a type of `any` and // datasources should validate their arguments - initialize: (state?: P) => Promise; + initialize: (state?: P, savedObjectReferences?: SavedObjectReference[]) => Promise; // Given the current state, which parts should be saved? - getPersistableState: (state: T) => P; + getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; insertLayer: (state: T, newLayerId: string) => T; removeLayer: (state: T, layerId: string) => T; @@ -166,8 +161,6 @@ export interface Datasource { toExpression: (state: T, layerId: string) => Ast | string | null; - getMetaData: (state: T) => DatasourceMetaData; - getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; @@ -408,7 +401,7 @@ export interface VisualizationType { label: string; } -export interface Visualization { +export interface Visualization { /** Plugin ID, such as "lnsXY" */ id: string; @@ -418,11 +411,7 @@ export interface Visualization { * - Loadingn from a saved visualization * - When using suggestions, the suggested state is passed in */ - initialize: (frame: FramePublicAPI, state?: P) => T; - /** - * Can remove any state that should not be persisted to saved object, such as UI state - */ - getPersistableState: (state: T) => P; + initialize: (frame: FramePublicAPI, state?: T) => T; /** * Visualizations must provide at least one type for the chart switcher, @@ -504,12 +493,18 @@ export interface Visualization { */ getSuggestions: (context: SuggestionRequest) => Array>; - toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; + toExpression: ( + state: T, + datasourceLayers: Record + ) => Ast | string | null; /** * Expression to render a preview version of the chart in very constrained space. * If there is no expression provided, the preview icon is used. */ - toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; + toPreviewExpression?: ( + state: T, + datasourceLayers: Record + ) => Ast | string | null; } export interface LensFilterEvent { diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 876d1141740e1..f579085646f6f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -53,7 +53,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toMatchSnapshot(); }); @@ -74,7 +74,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast).chain[0].arguments.fittingFunction[0] ).toEqual('None'); }); @@ -94,7 +94,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast; expect(expression.chain[0].arguments.showXAxisTitle[0]).toBe(true); expect(expression.chain[0].arguments.showYAxisTitle[0]).toBe(true); @@ -116,7 +116,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toBeNull(); }); @@ -137,7 +137,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toBeNull(); }); @@ -157,7 +157,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers )! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); @@ -191,7 +191,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast; expect( (expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments @@ -216,7 +216,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast; expect( (expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 9b9c159af265e..cd32d4f94c3e5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -7,13 +7,16 @@ import { Ast } from '@kbn/interpreter/common'; import { ScaleType } from '@elastic/charts'; import { State, LayerConfig } from './types'; -import { FramePublicAPI, OperationMetadata } from '../types'; +import { OperationMetadata, DatasourcePublicAPI } from '../types'; interface ValidLayer extends LayerConfig { xAccessor: NonNullable; } -export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => { +export const toExpression = ( + state: State, + datasourceLayers: Record +): Ast | null => { if (!state || !state.layers.length) { return null; } @@ -21,19 +24,20 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => const metadata: Record> = {}; state.layers.forEach((layer) => { metadata[layer.layerId] = {}; - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; datasource.getTableSpec().forEach((column) => { - const operation = frame.datasourceLayers[layer.layerId].getOperationForColumnId( - column.columnId - ); + const operation = datasourceLayers[layer.layerId].getOperationForColumnId(column.columnId); metadata[layer.layerId][column.columnId] = operation; }); }); - return buildExpression(state, metadata, frame); + return buildExpression(state, metadata, datasourceLayers); }; -export function toPreviewExpression(state: State, frame: FramePublicAPI) { +export function toPreviewExpression( + state: State, + datasourceLayers: Record +) { return toExpression( { ...state, @@ -44,7 +48,7 @@ export function toPreviewExpression(state: State, frame: FramePublicAPI) { isVisible: false, }, }, - frame + datasourceLayers ); } @@ -77,7 +81,7 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S export const buildExpression = ( state: State, metadata: Record>, - frame?: FramePublicAPI + datasourceLayers?: Record ): Ast | null => { const validLayers = state.layers.filter((layer): layer is ValidLayer => Boolean(layer.xAccessor && layer.accessors.length) @@ -149,8 +153,8 @@ export const buildExpression = ( layers: validLayers.map((layer) => { const columnToLabel: Record = {}; - if (frame) { - const datasource = frame.datasourceLayers[layer.layerId]; + if (datasourceLayers) { + const datasource = datasourceLayers[layer.layerId]; layer.accessors .concat(layer.splitAccessor ? [layer.splitAccessor] : []) .forEach((accessor) => { @@ -162,8 +166,8 @@ export const buildExpression = ( } const xAxisOperation = - frame && - frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); + datasourceLayers && + datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor); const isHistogramDimension = Boolean( xAxisOperation && diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index ab689ceb183be..2739ffe42f13f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -339,7 +339,6 @@ export interface XYState { } export type State = XYState; -export type PersistableState = XYState; export const visualizationTypes: VisualizationType[] = [ { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 0a8e8bbe0c46f..53f7a23dcae98 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -157,12 +157,6 @@ describe('xy_visualization', () => { }); }); - describe('#getPersistableState', () => { - it('persists the state as given', () => { - expect(xyVisualization.getPersistableState(exampleState())).toEqual(exampleState()); - }); - }); - describe('#removeLayer', () => { it('removes the specified layer', () => { const prevState: State = { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index f321e0962caa8..8c551c575764e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import { Visualization, OperationMetadata, VisualizationType } from '../types'; -import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; +import { State, SeriesType, visualizationTypes, LayerConfig } from './types'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; import chartMixedSVG from '../assets/chart_mixed_xy.svg'; import { isHorizontalChart } from './state_helpers'; @@ -74,7 +74,7 @@ function getDescription(state?: State) { }; } -export const xyVisualization: Visualization = { +export const xyVisualization: Visualization = { id: 'lnsXY', visualizationTypes, @@ -159,8 +159,6 @@ export const xyVisualization: Visualization = { ); }, - getPersistableState: (state) => state, - getConfiguration(props) { const layer = props.state.layers.find((l) => l.layerId === props.layerId)!; return { diff --git a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap new file mode 100644 index 0000000000000..4979438dbd3d0 --- /dev/null +++ b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Lens migrations 7.10.0 references should produce a valid document 1`] = ` +Object { + "attributes": Object { + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "3b7791e9-326e-40d5-a787-b7594e48d906": Object { + "columnOrder": Array [ + "77d8383e-f66e-471e-ae50-c427feedb5ba", + "a5c1b82d-51de-4448-a99d-6391432c3a03", + ], + "columns": Object { + "77d8383e-f66e-471e-ae50-c427feedb5ba": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geoip.country_iso_code", + "operationType": "terms", + "params": Object { + "orderBy": Object { + "columnId": "a5c1b82d-51de-4448-a99d-6391432c3a03", + "type": "column", + }, + "orderDirection": "desc", + "size": 5, + }, + "scale": "ordinal", + "sourceField": "geoip.country_iso_code", + }, + "a5c1b82d-51de-4448-a99d-6391432c3a03": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "Records", + }, + }, + }, + "9a27f85d-35a9-4246-81b2-48e7ee9b0707": Object { + "columnOrder": Array [ + "96352896-c508-4fca-90d8-66e9ebfce621", + "4ce9b4c7-2ebf-4d48-8669-0ea69d973353", + ], + "columns": Object { + "4ce9b4c7-2ebf-4d48-8669-0ea69d973353": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "Records", + }, + "96352896-c508-4fca-90d8-66e9ebfce621": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.src", + "operationType": "terms", + "params": Object { + "orderBy": Object { + "columnId": "4ce9b4c7-2ebf-4d48-8669-0ea69d973353", + "type": "column", + }, + "orderDirection": "desc", + "size": 5, + }, + "scale": "ordinal", + "sourceField": "geo.src", + }, + }, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "indexRefName": "filter-index-pattern-0", + "key": "geo.src", + "negate": true, + "params": Object { + "query": "CN", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "geo.src": "CN", + }, + }, + }, + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": undefined, + "indexRefName": "filter-index-pattern-1", + "key": "geoip.country_iso_code", + "negate": true, + "params": Object { + "query": "US", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "geoip.country_iso_code": "US", + }, + }, + }, + ], + "query": Object { + "language": "kuery", + "query": "NOT bytes > 5000", + }, + "visualization": Object { + "fittingFunction": "None", + "layers": Array [ + Object { + "accessors": Array [ + "4ce9b4c7-2ebf-4d48-8669-0ea69d973353", + ], + "layerId": "9a27f85d-35a9-4246-81b2-48e7ee9b0707", + "position": "top", + "seriesType": "bar", + "showGridlines": false, + "xAccessor": "96352896-c508-4fca-90d8-66e9ebfce621", + }, + Object { + "accessors": Array [ + "a5c1b82d-51de-4448-a99d-6391432c3a03", + ], + "layerId": "3b7791e9-326e-40d5-a787-b7594e48d906", + "seriesType": "bar", + "xAccessor": "77d8383e-f66e-471e-ae50-c427feedb5ba", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar", + }, + }, + "title": "mylens", + "visualizationType": "lnsXY", + }, + "references": Array [ + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906", + "type": "index-pattern", + }, + Object { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707", + "type": "index-pattern", + }, + Object { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "filter-index-pattern-0", + "type": "index-pattern", + }, + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "filter-index-pattern-1", + "type": "index-pattern", + }, + ], + "type": "lens", +} +`; diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index 0541d9636577b..676494dcab619 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -278,4 +278,233 @@ describe('Lens migrations', () => { expect(result).toEqual(input); }); }); + + describe('7.10.0 references', () => { + const context = {} as SavedObjectMigrationContext; + + const example = { + attributes: { + description: '', + expression: + 'kibana\n| kibana_context query="{\\"query\\":\\"NOT bytes > 5000\\",\\"language\\":\\"kuery\\"}" \n filters="[{\\"meta\\":{\\"index\\":\\"90943e30-9a47-11e8-b64d-95841ca0b247\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geo.src\\",\\"params\\":{\\"query\\":\\"CN\\"}},\\"query\\":{\\"match_phrase\\":{\\"geo.src\\":\\"CN\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}},{\\"meta\\":{\\"index\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geoip.country_iso_code\\",\\"params\\":{\\"query\\":\\"US\\"}},\\"query\\":{\\"match_phrase\\":{\\"geoip.country_iso_code\\":\\"US\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}]"\n| lens_merge_tables layerIds="9a27f85d-35a9-4246-81b2-48e7ee9b0707"\n layerIds="3b7791e9-326e-40d5-a787-b7594e48d906" \n tables={esaggs index="90943e30-9a47-11e8-b64d-95841ca0b247" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geo.src\\",\\"orderBy\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-96352896-c508-4fca-90d8-66e9ebfce621\\":{\\"label\\":\\"Top values of geo.src\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geo.src\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\"},\\"col-1-4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"}}"}\n tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geoip.country_iso_code\\",\\"orderBy\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-77d8383e-f66e-471e-ae50-c427feedb5ba\\":{\\"label\\":\\"Top values of geoip.country_iso_code\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geoip.country_iso_code\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\"},\\"col-1-a5c1b82d-51de-4448-a99d-6391432c3a03\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"}}"}\n| lens_xy_chart xTitle="Top values of geo.src" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} fittingFunction="None" \n layers={lens_xy_layer layerId="9a27f85d-35a9-4246-81b2-48e7ee9b0707" hide=false xAccessor="96352896-c508-4fca-90d8-66e9ebfce621" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="4ce9b4c7-2ebf-4d48-8669-0ea69d973353" columnToLabel="{\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":\\"Count of records\\"}"}\n layers={lens_xy_layer layerId="3b7791e9-326e-40d5-a787-b7594e48d906" hide=false xAccessor="77d8383e-f66e-471e-ae50-c427feedb5ba" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="a5c1b82d-51de-4448-a99d-6391432c3a03" columnToLabel="{\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\":\\"Count of records [1]\\"}"}', + state: { + datasourceMetaData: { + filterableIndexPatterns: [ + { id: '90943e30-9a47-11e8-b64d-95841ca0b247', title: 'kibana_sample_data_logs' }, + { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' }, + ], + }, + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + layers: { + '3b7791e9-326e-40d5-a787-b7594e48d906': { + columnOrder: [ + '77d8383e-f66e-471e-ae50-c427feedb5ba', + 'a5c1b82d-51de-4448-a99d-6391432c3a03', + ], + columns: { + '77d8383e-f66e-471e-ae50-c427feedb5ba': { + dataType: 'string', + isBucketed: true, + label: 'Top values of geoip.country_iso_code', + operationType: 'terms', + params: { + orderBy: { + columnId: 'a5c1b82d-51de-4448-a99d-6391432c3a03', + type: 'column', + }, + orderDirection: 'desc', + size: 5, + }, + scale: 'ordinal', + sourceField: 'geoip.country_iso_code', + }, + 'a5c1b82d-51de-4448-a99d-6391432c3a03': { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + }, + '9a27f85d-35a9-4246-81b2-48e7ee9b0707': { + columnOrder: [ + '96352896-c508-4fca-90d8-66e9ebfce621', + '4ce9b4c7-2ebf-4d48-8669-0ea69d973353', + ], + columns: { + '4ce9b4c7-2ebf-4d48-8669-0ea69d973353': { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + '96352896-c508-4fca-90d8-66e9ebfce621': { + dataType: 'string', + isBucketed: true, + label: 'Top values of geo.src', + operationType: 'terms', + params: { + orderBy: { + columnId: '4ce9b4c7-2ebf-4d48-8669-0ea69d973353', + type: 'column', + }, + orderDirection: 'desc', + size: 5, + }, + scale: 'ordinal', + sourceField: 'geo.src', + }, + }, + indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', + }, + }, + }, + }, + filters: [ + { + $state: { store: 'appState' }, + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.src', + negate: true, + params: { query: 'CN' }, + type: 'phrase', + }, + query: { match_phrase: { 'geo.src': 'CN' } }, + }, + { + $state: { store: 'appState' }, + meta: { + alias: null, + disabled: false, + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + key: 'geoip.country_iso_code', + negate: true, + params: { query: 'US' }, + type: 'phrase', + }, + query: { match_phrase: { 'geoip.country_iso_code': 'US' } }, + }, + ], + query: { language: 'kuery', query: 'NOT bytes > 5000' }, + visualization: { + fittingFunction: 'None', + layers: [ + { + accessors: ['4ce9b4c7-2ebf-4d48-8669-0ea69d973353'], + layerId: '9a27f85d-35a9-4246-81b2-48e7ee9b0707', + position: 'top', + seriesType: 'bar', + showGridlines: false, + xAccessor: '96352896-c508-4fca-90d8-66e9ebfce621', + }, + { + accessors: ['a5c1b82d-51de-4448-a99d-6391432c3a03'], + layerId: '3b7791e9-326e-40d5-a787-b7594e48d906', + seriesType: 'bar', + xAccessor: '77d8383e-f66e-471e-ae50-c427feedb5ba', + }, + ], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'bar', + }, + }, + title: 'mylens', + visualizationType: 'lnsXY', + }, + type: 'lens', + }; + + it('should remove expression', () => { + const result = migrations['7.10.0'](example, context); + expect(result.attributes.expression).toBeUndefined(); + }); + + it('should list references for layers', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.references?.find( + (ref) => ref.name === 'indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906' + )?.id + ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f'); + expect( + result.references?.find( + (ref) => ref.name === 'indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707' + )?.id + ).toEqual('90943e30-9a47-11e8-b64d-95841ca0b247'); + }); + + it('should remove index pattern ids from layers', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.attributes.state.datasourceStates.indexpattern.layers[ + '3b7791e9-326e-40d5-a787-b7594e48d906' + ].indexPatternId + ).toBeUndefined(); + expect( + result.attributes.state.datasourceStates.indexpattern.layers[ + '9a27f85d-35a9-4246-81b2-48e7ee9b0707' + ].indexPatternId + ).toBeUndefined(); + }); + + it('should remove datsource meta data', () => { + const result = migrations['7.10.0'](example, context); + expect(result.attributes.state.datasourceMetaData).toBeUndefined(); + }); + + it('should list references for filters', () => { + const result = migrations['7.10.0'](example, context); + expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-0')?.id).toEqual( + '90943e30-9a47-11e8-b64d-95841ca0b247' + ); + expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-1')?.id).toEqual( + 'ff959d40-b880-11e8-a6d9-e546fe2bba5f' + ); + }); + + it('should remove index pattern ids from filters', () => { + const result = migrations['7.10.0'](example, context); + expect(result.attributes.state.filters[0].meta.index).toBeUndefined(); + expect(result.attributes.state.filters[0].meta.indexRefName).toEqual( + 'filter-index-pattern-0' + ); + expect(result.attributes.state.filters[1].meta.index).toBeUndefined(); + expect(result.attributes.state.filters[1].meta.indexRefName).toEqual( + 'filter-index-pattern-1' + ); + }); + + it('should list reference for current index pattern', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.references?.find( + (ref) => ref.name === 'indexpattern-datasource-current-indexpattern' + )?.id + ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f'); + }); + + it('should remove current index pattern id from datasource state', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.attributes.state.datasourceStates.indexpattern.currentIndexPatternId + ).toBeUndefined(); + }); + + it('should produce a valid document', () => { + const result = migrations['7.10.0'](example, context); + // changes to the outcome of this are critical - this test is a safe guard to not introduce changes accidentally + // if this test fails, make extra sure it's expected + expect(result).toMatchSnapshot(); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index d24a3e92cbd9c..fdbfa1e455f60 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -6,11 +6,16 @@ import { cloneDeep } from 'lodash'; import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { SavedObjectMigrationMap, SavedObjectMigrationFn } from 'src/core/server'; +import { + SavedObjectMigrationMap, + SavedObjectMigrationFn, + SavedObjectReference, + SavedObjectUnsanitizedDoc, +} from 'src/core/server'; +import { Query, Filter } from 'src/plugins/data/public'; +import { PersistableFilter } from '../common'; -interface LensDocShape { - id?: string; - type?: string; +interface LensDocShapePre710 { visualizationType: string | null; title: string; expression: string | null; @@ -21,18 +26,44 @@ interface LensDocShape { datasourceStates: { // This is hardcoded as our only datasource indexpattern: { + currentIndexPatternId: string; layers: Record< string, { columnOrder: string[]; columns: Record; + indexPatternId: string; } >; }; }; visualization: VisualizationState; - query: unknown; - filters: unknown[]; + query: Query; + filters: Filter[]; + }; +} + +interface LensDocShape { + id?: string; + type?: string; + visualizationType: string | null; + title: string; + state: { + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + layers: Record< + string, + { + columnOrder: string[]; + columns: Record; + } + >; + }; + }; + visualization: VisualizationState; + query: Query; + filters: PersistableFilter[]; }; } @@ -55,7 +86,10 @@ interface XYStatePost77 { * Removes the `lens_auto_date` subexpression from a stored expression * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} */ -const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { +const removeLensAutoDate: SavedObjectMigrationFn = ( + doc, + context +) => { const expression = doc.attributes.expression; if (!expression) { return doc; @@ -112,7 +146,10 @@ const removeLensAutoDate: SavedObjectMigrationFn = ( /** * Adds missing timeField arguments to esaggs in the Lens expression */ -const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { +const addTimeFieldToEsaggs: SavedObjectMigrationFn = ( + doc, + context +) => { const expression = doc.attributes.expression; if (!expression) { return doc; @@ -174,14 +211,14 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn = }; const removeInvalidAccessors: SavedObjectMigrationFn< - LensDocShape, - LensDocShape + LensDocShapePre710, + LensDocShapePre710 > = (doc) => { const newDoc = cloneDeep(doc); if (newDoc.attributes.visualizationType === 'lnsXY') { const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {}; const xyState = newDoc.attributes.state.visualization; - (newDoc.attributes as LensDocShape< + (newDoc.attributes as LensDocShapePre710< XYStatePost77 >).state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => { const layerId = layer.layerId; @@ -197,9 +234,86 @@ const removeInvalidAccessors: SavedObjectMigrationFn< return newDoc; }; +const extractReferences: SavedObjectMigrationFn = ({ + attributes, + references, + ...docMeta +}) => { + const savedObjectReferences: SavedObjectReference[] = []; + // add currently selected index pattern to reference list + savedObjectReferences.push({ + type: 'index-pattern', + id: attributes.state.datasourceStates.indexpattern.currentIndexPatternId, + name: 'indexpattern-datasource-current-indexpattern', + }); + + // add layer index patterns to list and remove index pattern ids from layers + const persistableLayers: Record< + string, + Omit< + LensDocShapePre710['state']['datasourceStates']['indexpattern']['layers'][string], + 'indexPatternId' + > + > = {}; + Object.entries(attributes.state.datasourceStates.indexpattern.layers).forEach( + ([layerId, { indexPatternId, ...persistableLayer }]) => { + savedObjectReferences.push({ + type: 'index-pattern', + id: indexPatternId, + name: `indexpattern-datasource-layer-${layerId}`, + }); + persistableLayers[layerId] = persistableLayer; + } + ); + + // add filter index patterns to reference list and remove index pattern ids from filter definitions + const persistableFilters = attributes.state.filters.map((filterRow, i) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `filter-index-pattern-${i}`; + savedObjectReferences.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }); + + // put together new saved object format + const newDoc: SavedObjectUnsanitizedDoc = { + ...docMeta, + references: savedObjectReferences, + attributes: { + visualizationType: attributes.visualizationType, + title: attributes.title, + state: { + datasourceStates: { + indexpattern: { + layers: persistableLayers, + }, + }, + visualization: attributes.state.visualization, + query: attributes.state.query, + filters: persistableFilters, + }, + }, + }; + + return newDoc; +}; + export const migrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). '7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context), + '7.10.0': extractReferences, }; From 3031e668060ad56124a0f191f1c03cea71532697 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 21 Aug 2020 09:16:17 -0700 Subject: [PATCH 08/25] Update datasets UI copy to data streams (#75618) --- x-pack/plugins/ingest_manager/dev_docs/definitions.md | 6 +++--- .../ingest_manager/constants/page_paths.ts | 4 ++-- .../ingest_manager/hooks/use_breadcrumbs.tsx | 2 +- .../applications/ingest_manager/layouts/default.tsx | 2 +- .../sections/data_stream/list_page/index.tsx | 10 +++++----- .../standalone_instructions.tsx | 2 +- .../overview/components/datastream_section.tsx | 8 ++++---- .../public/applications/ingest_manager/types/index.ts | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md index be5aeb923e903..d9ff597c5e84b 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/definitions.md +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -13,9 +13,9 @@ definitions for one or multiple inputs and each input can contain one or multipl With the example of the nginx Package policy, it contains two inputs: `logs` and `nginx/metrics`. Logs and metrics are collected differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. -## Data Stream +## Data stream -Data Streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify +Data streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify ingesting data and the setup of Elasticsearch. ## Elastic Agent @@ -35,7 +35,7 @@ Fleet is the part of the Ingest Manager UI in Kibana that handles the part of en Ingest Management + Elastic Agent follow a strict new indexing strategy: `{type}-{dataset}-{namespace}`. An example for this is `logs-nginx.access-default`. More details about it can be found in the Index Strategy below. All data of -the index strategy is sent to Data Streams. +the index strategy is sent to data streams. ## Input diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts index 4a8dcfedc0936..c370f46b9f5a0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -53,7 +53,7 @@ export const PAGE_ROUTING_PATHS = { fleet_agent_details_events: '/fleet/agents/:agentId', fleet_agent_details_details: '/fleet/agents/:agentId/details', fleet_enrollment_tokens: '/fleet/enrollment-tokens', - data_streams: '/datasets', + data_streams: '/data-streams', }; export const pagePathGetters: { @@ -80,5 +80,5 @@ export const pagePathGetters: { fleet_agent_details: ({ agentId, tabId }) => `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', - data_streams: () => '/datasets', + data_streams: () => '/data-streams', }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index 6ef1351dc5b60..1d80495d2b347 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -207,7 +207,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { - defaultMessage: 'Datasets', + defaultMessage: 'Data streams', }), }, ], 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 726da7a790b97..30294779d1a3d 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 @@ -96,7 +96,7 @@ export const DefaultLayout: React.FunctionComponent = ({ 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 d8ab46fbf87f7..53fd6a713b523 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 @@ -32,7 +32,7 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => (

@@ -173,7 +173,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {

} @@ -216,14 +216,14 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { isLoading ? ( ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( emptyPrompt ) : ( ) } @@ -253,7 +253,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { placeholder: i18n.translate( 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', { - defaultMessage: 'Filter datasets', + defaultMessage: 'Filter data streams', } ), incremental: true, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx index abe834e7db19c..049ceca82b309 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -157,7 +157,7 @@ export const StandaloneInstructions: React.FunctionComponent = ({ agentPo diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx index 41c011de2da5c..bece6ec074b88 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx @@ -47,14 +47,14 @@ export const OverviewDatastreamSection: React.FC = () => { @@ -65,7 +65,7 @@ export const OverviewDatastreamSection: React.FC = () => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 80e27b7c4d0bf..30a6742af6ea6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -43,7 +43,7 @@ export { CreatePackagePolicyResponse, UpdatePackagePolicyRequest, UpdatePackagePolicyResponse, - // API schemas - Data Streams + // API schemas - Data streams GetDataStreamsResponse, // API schemas - Agents GetAgentsResponse, From 82e30f6effdecbc8d9c6a7d4a5f53fb267b60541 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 21 Aug 2020 10:48:04 -0600 Subject: [PATCH 09/25] Upgrade EUI to v27.4.1 (#75240) * eui to 27.4.1 * src snapshot updates * x-pack snapshot updates * remove increased default timeout * revert date change * delete default_timeout file * reinstate storyshot Co-authored-by: Elastic Machine --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- .../collapsible_nav.test.tsx.snap | 7748 +++++------------ .../header/__snapshots__/header.test.tsx.snap | 1281 ++- .../flyout_service.test.tsx.snap | 4 +- .../__snapshots__/modal_service.test.tsx.snap | 6 +- src/dev/jest/config.js | 1 - src/dev/jest/setup/default_timeout.js | 25 - .../__snapshots__/new_vis_modal.test.tsx.snap | 5080 +++-------- .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_sample_panel_action/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- x-pack/dev-tools/jest/create_jest_config.js | 1 - x-pack/package.json | 2 +- .../TransactionActionMenu.test.tsx.snap | 5 +- .../asset_manager.stories.storyshot | 1052 +-- .../custom_element_modal.stories.storyshot | 1888 ++-- .../keyboard_shortcuts_doc.stories.storyshot | 2207 +++-- .../saved_elements_modal.stories.storyshot | 1362 ++- .../__snapshots__/settings.test.tsx.snap | 898 +- .../upload_license.test.tsx.snap | 989 +-- .../report_info_button.test.tsx.snap | 1236 +-- .../helpers/home.helpers.ts | 2 +- .../__snapshots__/ml_flyout.test.tsx.snap | 261 +- yarn.lock | 8 +- 25 files changed, 7978 insertions(+), 16088 deletions(-) delete mode 100644 src/dev/jest/setup/default_timeout.js diff --git a/package.json b/package.json index 23fc31f369b8d..46418e52d8548 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.0-rc.2", "@elastic/ems-client": "7.9.3", - "@elastic/eui": "27.4.0", + "@elastic/eui": "27.4.1", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index a37281cb2263f..531513481b1d4 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "19.8.1", - "@elastic/eui": "27.4.0", + "@elastic/eui": "27.4.1", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 72d62730fa698..2cfe232bf5653 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -378,9 +378,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + +
+
+ +
+ +
+
- +
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - returnFocus={[Function]} - shards={Array []} - sideCar={ - Object { - "assignMedium": [Function], - "assignSyncMedium": [Function], - "options": Object { - "async": true, - "ssr": false, - }, - "read": [Function], - "useMedium": [Function], - } - } + + + + + +
+
+ +
+ + + + + - - -
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - returnFocus={[Function]} - shards={Array []} +

- - + Recently viewed +

+
- } - onActivation={[Function]} - onDeactivation={[Function]} - persistentFocus={false} - returnFocus={[Function]} - shards={Array []} - /> - - - - + + + + + +
+ -
-
+
+ + + + + + +
+
+ +
+ + + + + + + -
- - + Kibana + +
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-kibana" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
+
+ +
+
+ +
+
-
- -
-
- -
-
+ visualize + + + + + +
  • -
    - - - -
    -
  • -
    -
    -
    + dashboard + + + + + +
    - - - +
    + +
    +
    + + + + + + + + + +

    + Observability +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-observability" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + -
    -
    + +
    + + +
    + - -
    -
    -
    - - - -
    -
    -
    -
    -
    + Observability + +
    -
    -
    - +
    + + + +
    +
    + +
    +
    +
    - - - - - - -

    - Observability -

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

    + Security +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-security" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + -
    -
    + +
    + + +
    + - -
    -
    -
    - - - -
    -
    -
    -
    -
    + Security + +
    -
    -
    - + + + + + +
    + +
    +
    +
    - - - -

    - Management -

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

    + Management +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-management" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    -
    -
    - - - -
    -
    - - + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + +
    +
    +
    + + +
    +
    + +
      +
      + - -
        - -
        - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
      • - -
      • - -
      -
      -
      -
    - - -
    - - - + , } } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" > - - - - + + + + +
    -
    -
    - - + + +
    + + + + + + + + @@ -5439,621 +2471,442 @@ exports[`CollapsibleNav renders the default nav 2`] = ` clickOutsideDisables={true} disabled={false} > - - -
    - - - +
    -
    - -
    -
    +
    + +
    + + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + -
    -
    - -
    -
    -
    - -
    - -
    -

    - No recently viewed items -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    + Recently viewed + +
    -
    -
    - -
    -
    - +
    + + + +
    +
    + +
    +
    +
    -
    - -
    -
    - -
      - -
      - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • - -
    -
    -
    +

    + No recently viewed items +

    -
    -
    -
    - - +
    + +
    +
    +
    + + + + + + +
    +
    + +
    + + +
    +
    + +
      - +
      + + Dock navigation + + , } } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" > - - - - + + + +
    +
    - -
    - - + + +
    + + + +
    - - +
    + + + + close + + + + + + + + +
    @@ -6299,581 +3152,442 @@ exports[`CollapsibleNav renders the default nav 3`] = ` clickOutsideDisables={true} disabled={true} > - - -
    - - - +
    -
    + - + + +
    +
    + +
    + + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    + - -
    - - - +

    + No recently viewed items +

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

      - Recently viewed -

      -
      -
      - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" - > -
      -
      - -
      -
      - -
      -
      -
      - -
      - -
      -

      - No recently viewed items -

      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      - -
      - , } - > - -
      -
      - -
        - -
        - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
      • - -
      • - -
      -
      -
      -
      -
      -
      -
      -
      - - - - - - + + + +
    +
    - -
    + + +
    + + + +
    - - + } + > + + +
    + + + + close + + + + + + + + +
    diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index a1920154d9f71..c02a763c40101 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -8999,766 +8999,635 @@ exports[`Header renders 3`] = ` clickOutsideDisables={true} disabled={true} > - - -
    - - - +
    + +
      + +
    • + +
    • +
      +
    +
    +
    +
    + +
    + + +
    +
    + +
    + +
    + } + >
    - -
    -
    +
    + +
    + + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" + initialIsOpen={true} + onToggle={[Function]} + paddingSize="none" + > +
    +
    + +
    +
    + +
    +
    +
    -
    - -
    -
    - - - -
    -
    -
    -
    - - + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    + +
      + - - - -

      - Recently viewed -

      -
      -
      - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - onToggle={[Function]} - paddingSize="none" +
    • -
      -
      - +
    • +
      +
    +
    +
    +
    +
    + + +
    +
    + +
      + +
      - -
      - - - - -
      - -
      - -

      - Recently viewed -

      -
      -
      -
      -
      -
      + Undock navigation
      - -
      -
      - -
      -
      -
      - - - -
      -
      -
      -
      -
      -
      - - - -
      -
      - -
      - -
      -
      - -
        - -
      • - -
      • -
        -
      -
      -
      -
      -
      - , } - > - -
      -
      - -
        - -
        - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
      • - -
      • - -
      -
      -
      -
      -
      -
      -
      -
      - - - - - - + + +
      +
    +
    - -
    + + +
    + + + +
    - - + } + > + + +
    + + + + close + + + + + + + + +
    diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index fa83b34e06b81..a5c1d46f74709 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -26,7 +26,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index fb00ddc38c6dc..aea52eb8e7ab7 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -31,7 +31,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
    Modal content
    "`; +exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
    Modal content
    "`; exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ @@ -53,7 +53,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

    Some message

    "`; +exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

    Some message

    "`; exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ @@ -145,7 +145,7 @@ Array [ ] `; -exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
    Modal content
    "`; +exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
    Modal content
    "`; exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index d46b955f6668d..74e1ec5e2b4ed 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -78,7 +78,6 @@ export default { setupFilesAfterEnv: [ '/src/dev/jest/setup/mocks.js', '/src/dev/jest/setup/react_testing_library.js', - '/src/dev/jest/setup/default_timeout.js', ], coverageDirectory: '/target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], diff --git a/src/dev/jest/setup/default_timeout.js b/src/dev/jest/setup/default_timeout.js deleted file mode 100644 index eea38e745b960..0000000000000 --- a/src/dev/jest/setup/default_timeout.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-env jest */ - -/** - * Set the default timeout for the unit tests to 30 seconds, temporarily - */ -jest.setTimeout(30 * 1000); diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index a27dfa13e743e..6aed16e937713 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -142,21 +142,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiOverlayMask euiOverlayMask--aboveHeader" >