From 827442b4b064eabaabd9da2a511a97f8b4e765ef Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 4 Jun 2021 20:34:52 -0600 Subject: [PATCH 1/7] [Security Solution][Detection Engine] Test cases for alias failure test cases where we don't copy aliases correctly (#101437) ## Summary Test cases for signals and aliases, including a failure of where we do not copy alias data at the moment even if the target is an ECS compatible field. For example with this mapping: ```json { "dynamic": "strict", "properties": { "@timestamp": { "type": "date" }, "host": { "properties": { "name": { "type": "alias", "path": "host_alias.name" } } }, "host_alias": { "properties": { "name": { "type": "keyword" } } } } } ``` If we detect this as a signal hit we should be copying over both: * `host_alias.name` -> `host.name` * `host_alias.name` -> `host_alias.name` to the target signal index, but we only copy: * `host_alias.name` -> `host_alias.name` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../security_and_spaces/tests/aliases.ts | 65 +++++++++++++++++++ .../security_and_spaces/tests/index.ts | 1 + .../security_solution/alias/data.json | 59 +++++++++++++++++ .../security_solution/alias/mappings.json | 36 ++++++++++ 4 files changed, 161 insertions(+) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts create mode 100644 x-pack/test/functional/es_archives/security_solution/alias/data.json create mode 100644 x-pack/test/functional/es_archives/security_solution/alias/mappings.json diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts new file mode 100644 index 0000000000000..d21253199d733 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + interface HostAlias { + name: string; + } + + describe('Tests involving aliases of source indexes and the signals index', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await esArchiver.load('security_solution/alias'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('security_solution/alias'); + }); + + it('Should keep the original alias value such as "host_alias" from a source index when the value is indexed', async () => { + const rule = getRuleForSignalTesting(['alias']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map( + (signal) => (signal._source.host_alias as HostAlias).name + ); + expect(hits).to.eql(['host name 1', 'host name 2', 'host name 3', 'host name 4']); + }); + + // TODO: Make aliases work to where we can have ECS fields such as host.name filled out + it.skip('Should copy alias data from a source index into the signals index in the same position when the target is ECS compatible', async () => { + const rule = getRuleForSignalTesting(['alias']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((signal) => (signal._source.host as HostAlias).name); + expect(hits).to.eql(['host name 1', 'host name 2', 'host name 3', 'host name 4']); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 00b289a89e4c8..01fa2765ba0f0 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -13,6 +13,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('', function () { this.tags('ciGroup11'); + loadTestFile(require.resolve('./aliases')); loadTestFile(require.resolve('./add_actions')); loadTestFile(require.resolve('./update_actions')); loadTestFile(require.resolve('./add_prepackaged_rules')); diff --git a/x-pack/test/functional/es_archives/security_solution/alias/data.json b/x-pack/test/functional/es_archives/security_solution/alias/data.json new file mode 100644 index 0000000000000..a8bd64cb044eb --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/alias/data.json @@ -0,0 +1,59 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "alias", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "host_alias": { + "name": "host name 1" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "alias", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "host_alias": { + "name": "host name 2" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "alias", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "host_alias": { + "name": "host name 3" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "alias", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "host_alias": { + "name": "host name 4" + } + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/security_solution/alias/mappings.json b/x-pack/test/functional/es_archives/security_solution/alias/mappings.json new file mode 100644 index 0000000000000..280ec9377df64 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/alias/mappings.json @@ -0,0 +1,36 @@ +{ + "type": "index", + "value": { + "index": "host_alias", + "mappings": { + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "host": { + "properties": { + "name": { + "type": "alias", + "path": "host_alias.name" + } + } + }, + "host_alias": { + "properties": { + "name": { + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "refresh_interval": "1s", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} From 6338a598f14733a343d7de288949be3de3a01454 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 7 Jun 2021 12:39:01 +0300 Subject: [PATCH 2/7] [Timelion] Update the removal message to mention the exact version (#100994) * [Timelion] Update the removal message to mention the exact version * Fix typo Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/dashboard/timelion.asciidoc | 2 +- .../timelion/public/components/timelion_deprecation.tsx | 2 +- src/plugins/timelion/server/deprecations.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 675fd03df3648..ec9e8b56f9342 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -4,7 +4,7 @@ To use *Timelion*, you define a graph by chaining functions together, using the *Timelion*-specific syntax. The syntax enables some features that classical point series charts don't offer, such as pulling data from different indices or data sources into one graph. -deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In the last 7.x minor version and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."] +deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In 7.16 and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."] [float] ==== Timelion expressions diff --git a/src/plugins/timelion/public/components/timelion_deprecation.tsx b/src/plugins/timelion/public/components/timelion_deprecation.tsx index efcef88b3d0a2..117aabed6773c 100644 --- a/src/plugins/timelion/public/components/timelion_deprecation.tsx +++ b/src/plugins/timelion/public/components/timelion_deprecation.tsx @@ -19,7 +19,7 @@ export const TimelionDeprecation = ({ links }: DocLinksStart) => { title={ diff --git a/src/plugins/timelion/server/deprecations.ts b/src/plugins/timelion/server/deprecations.ts index e65d72cb460df..3c344e1d4a8d8 100644 --- a/src/plugins/timelion/server/deprecations.ts +++ b/src/plugins/timelion/server/deprecations.ts @@ -30,7 +30,7 @@ export const showWarningMessageIfTimelionSheetWasFound = async ( const count = await getTimelionSheetsCount(savedObjectsClient); if (count > 0) { logger.warn( - 'Deprecated since 7.0, the Timelion app will be removed in the last 7.x minor version. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html.' + 'Deprecated since 7.0, the Timelion app will be removed in 7.16. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html.' ); } }; @@ -49,7 +49,7 @@ export async function getDeprecations({ if (count > 0) { deprecations.push({ - message: `You have ${count} Timelion worksheets. The Timelion app will be removed in the last 7.x minor version. To continue using your Timelion worksheets, migrate them to a dashboard.`, + message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 7.16. To continue using your Timelion worksheets, migrate them to a dashboard.`, documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', level: 'warning', From 2a71047d9b6e4714158fdb0f0c176dd09333ea04 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 7 Jun 2021 13:15:13 +0200 Subject: [PATCH 3/7] Url service locators (#101045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add url service types * refactor: 💡 move locator types into its own folder * feat: 🎸 add abstract locator implementation * feat: 🎸 implement abstract locator client * feat: 🎸 add browser-side locators service * feat: 🎸 implement locator .getLocation() * feat: 🎸 implement navigate function * feat: 🎸 implement locator service in /common folder * feat: 🎸 expose locators client on browser and server * refactor: 💡 make locators async * chore: 🤖 add deprecation notice to URL generators * docs: ✏️ add deprecation notice to readme * test: 💍 make test locator async Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/discover/public/url_generator.ts | 14 +- .../url_service/__tests__/locators.test.ts | 165 ++++++++++++++++++ .../common/url_service/__tests__/setup.ts | 42 +++++ src/plugins/share/common/url_service/index.ts | 10 ++ .../common/url_service/locators/index.ts | 11 ++ .../common/url_service/locators/locator.ts | 69 ++++++++ .../url_service/locators/locator_client.ts | 47 +++++ .../common/url_service/locators/types.ts | 91 ++++++++++ .../share/common/url_service/url_service.ts | 23 +++ src/plugins/share/public/plugin.ts | 56 ++++-- .../share/public/url_generators/README.md | 6 + .../url_generators/url_generator_service.ts | 10 ++ src/plugins/share/server/plugin.ts | 29 ++- 13 files changed, 553 insertions(+), 20 deletions(-) create mode 100644 src/plugins/share/common/url_service/__tests__/locators.test.ts create mode 100644 src/plugins/share/common/url_service/__tests__/setup.ts create mode 100644 src/plugins/share/common/url_service/index.ts create mode 100644 src/plugins/share/common/url_service/locators/index.ts create mode 100644 src/plugins/share/common/url_service/locators/locator.ts create mode 100644 src/plugins/share/common/url_service/locators/locator_client.ts create mode 100644 src/plugins/share/common/url_service/locators/types.ts create mode 100644 src/plugins/share/common/url_service/url_service.ts diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index 21bdbf225d6aa..63dea20fecc0a 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -6,16 +6,10 @@ * Side Public License, v 1. */ -import { - TimeRange, - Filter, - Query, - esFilters, - QueryState, - RefreshInterval, -} from '../../data/public'; +import type { UrlGeneratorsDefinition } from '../../share/public'; +import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../share/public'; export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR'; @@ -71,10 +65,12 @@ export interface DiscoverUrlGeneratorState { * Used interval of the histogram */ interval?: string; + /** * Array of the used sorting [[field,direction],...] */ sort?: string[][]; + /** * id of the used saved query */ diff --git a/src/plugins/share/common/url_service/__tests__/locators.test.ts b/src/plugins/share/common/url_service/__tests__/locators.test.ts new file mode 100644 index 0000000000000..45d727df7de48 --- /dev/null +++ b/src/plugins/share/common/url_service/__tests__/locators.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { of } from 'src/plugins/kibana_utils/common'; +import { testLocator, TestLocatorState, urlServiceTestSetup } from './setup'; + +describe('locators', () => { + test('can start locators service', () => { + const { + service: { locators }, + } = urlServiceTestSetup(); + + expect(typeof locators).toBe('object'); + expect(typeof locators.create).toBe('function'); + expect(typeof locators.get).toBe('function'); + }); + + test('returns "undefined" for unregistered locator', () => { + const { + service: { locators }, + } = urlServiceTestSetup(); + + expect(locators.get(testLocator.id)).toBe(undefined); + }); + + test('can register a locator', () => { + const { + service: { locators }, + } = urlServiceTestSetup(); + + locators.create(testLocator); + expect(typeof locators.get(testLocator.id)).toBe('object'); + }); + + test('getLocation() returns KibanaLocation generated by the locator', async () => { + const { + service: { locators }, + } = urlServiceTestSetup(); + + locators.create(testLocator); + + const locator = locators.get(testLocator.id); + const location = await locator?.getLocation({ + savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + pageNumber: 21, + showFlyout: true, + }); + + expect(location).toEqual({ + app: 'test_app', + route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', + state: { isFlyoutOpen: true }, + }); + }); + + describe('.navigate()', () => { + test('throws if navigation method is not implemented', async () => { + const { + service: { locators }, + } = urlServiceTestSetup(); + const locator = locators.create(testLocator); + const [, error] = await of( + locator.navigate({ + pageNumber: 1, + savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + showFlyout: false, + }) + ); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe('not implemented'); + }); + + test('navigates user when .navigate() method is called', async () => { + const { + service: { locators }, + deps, + } = urlServiceTestSetup({ + navigate: jest.fn(async () => {}), + }); + const locator = locators.create(testLocator); + const [, error] = await of( + locator.navigate({ + pageNumber: 1, + savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + showFlyout: false, + }) + ); + + expect(error).toBe(undefined); + expect(deps.navigate).toHaveBeenCalledTimes(1); + expect(deps.navigate).toHaveBeenCalledWith( + { + app: 'test_app', + route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + state: { + isFlyoutOpen: false, + }, + }, + { replace: false } + ); + }); + + test('can specify "replace" navigation parameter', async () => { + const { + service: { locators }, + deps, + } = urlServiceTestSetup({ + navigate: jest.fn(async () => {}), + }); + const locator = locators.create(testLocator); + + await locator.navigate( + { + pageNumber: 1, + savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + showFlyout: false, + }, + { + replace: false, + } + ); + + expect(deps.navigate).toHaveBeenCalledTimes(1); + expect(deps.navigate).toHaveBeenCalledWith( + { + app: 'test_app', + route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + state: { + isFlyoutOpen: false, + }, + }, + { replace: false } + ); + + await locator.navigate( + { + pageNumber: 2, + savedObjectId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + showFlyout: false, + }, + { + replace: true, + } + ); + + expect(deps.navigate).toHaveBeenCalledTimes(2); + expect(deps.navigate).toHaveBeenCalledWith( + { + app: 'test_app', + route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', + state: { + isFlyoutOpen: false, + }, + }, + { replace: true } + ); + }); + }); +}); diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts new file mode 100644 index 0000000000000..ad13bb8d8d216 --- /dev/null +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import { LocatorDefinition } from '../locators'; +import { UrlService, UrlServiceDependencies } from '../url_service'; + +export interface TestLocatorState extends SerializableState { + savedObjectId: string; + showFlyout: boolean; + pageNumber: number; +} + +export const testLocator: LocatorDefinition = { + id: 'TEST_LOCATOR', + getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => { + return { + app: 'test_app', + route: `/my-object/${savedObjectId}?page=${pageNumber}`, + state: { + isFlyoutOpen: showFlyout, + }, + }; + }, +}; + +export const urlServiceTestSetup = (partialDeps: Partial = {}) => { + const deps: UrlServiceDependencies = { + navigate: async () => { + throw new Error('not implemented'); + }, + ...partialDeps, + }; + const service = new UrlService(deps); + + return { service, deps }; +}; diff --git a/src/plugins/share/common/url_service/index.ts b/src/plugins/share/common/url_service/index.ts new file mode 100644 index 0000000000000..84f74356bcf18 --- /dev/null +++ b/src/plugins/share/common/url_service/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './url_service'; +export * from './locators'; diff --git a/src/plugins/share/common/url_service/locators/index.ts b/src/plugins/share/common/url_service/locators/index.ts new file mode 100644 index 0000000000000..f9f87215eb4db --- /dev/null +++ b/src/plugins/share/common/url_service/locators/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './types'; +export * from './locator'; +export * from './locator_client'; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts new file mode 100644 index 0000000000000..68c3b05a7f411 --- /dev/null +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectReference } from 'kibana/server'; +import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import type { + LocatorDefinition, + LocatorPublic, + KibanaLocation, + LocatorNavigationParams, +} from './types'; + +export interface LocatorDependencies { + navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise; +} + +export class Locator

implements PersistableState

, LocatorPublic

{ + public readonly migrations: PersistableState

['migrations']; + + constructor( + public readonly definition: LocatorDefinition

, + protected readonly deps: LocatorDependencies + ) { + this.migrations = definition.migrations || {}; + } + + // PersistableState

------------------------------------------------------- + + public readonly telemetry: PersistableState

['telemetry'] = ( + state: P, + stats: Record + ): Record => { + return this.definition.telemetry ? this.definition.telemetry(state, stats) : stats; + }; + + public readonly inject: PersistableState

['inject'] = ( + state: P, + references: SavedObjectReference[] + ): P => { + return this.definition.inject ? this.definition.inject(state, references) : state; + }; + + public readonly extract: PersistableState

['extract'] = ( + state: P + ): { state: P; references: SavedObjectReference[] } => { + return this.definition.extract ? this.definition.extract(state) : { state, references: [] }; + }; + + // LocatorPublic

---------------------------------------------------------- + + public async getLocation(params: P): Promise { + return await this.definition.getLocation(params); + } + + public async navigate( + params: P, + { replace = false }: LocatorNavigationParams = {} + ): Promise { + const location = await this.getLocation(params); + await this.deps.navigate(location, { + replace, + }); + } +} diff --git a/src/plugins/share/common/url_service/locators/locator_client.ts b/src/plugins/share/common/url_service/locators/locator_client.ts new file mode 100644 index 0000000000000..168cc02d03ff1 --- /dev/null +++ b/src/plugins/share/common/url_service/locators/locator_client.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { LocatorDependencies } from './locator'; +import type { LocatorDefinition, LocatorPublic, ILocatorClient } from './types'; +import { Locator } from './locator'; + +export type LocatorClientDependencies = LocatorDependencies; + +export class LocatorClient implements ILocatorClient { + /** + * Collection of registered locators. + */ + protected locators: Map> = new Map(); + + constructor(protected readonly deps: LocatorClientDependencies) {} + + /** + * Creates and register a URL locator. + * + * @param definition A definition of URL locator. + * @returns A public interface of URL locator. + */ + public create

(definition: LocatorDefinition

): LocatorPublic

{ + const locator = new Locator

(definition, this.deps); + + this.locators.set(definition.id, locator); + + return locator; + } + + /** + * Returns a previously registered URL locator. + * + * @param id ID of a URL locator. + * @returns A public interface of a registered URL locator. + */ + public get

(id: string): undefined | LocatorPublic

{ + return this.locators.get(id); + } +} diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts new file mode 100644 index 0000000000000..d811ae0fd4aa2 --- /dev/null +++ b/src/plugins/share/common/url_service/locators/types.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; + +/** + * URL locator registry. + */ +export interface ILocatorClient { + /** + * Create and register a new locator. + * + * @param urlGenerator Definition of the new locator. + */ + create

(locatorDefinition: LocatorDefinition

): LocatorPublic

; + + /** + * Retrieve a previously registered locator. + * + * @param id Unique ID of the locator. + */ + get

(id: string): undefined | LocatorPublic

; +} + +/** + * A convenience interface used to define and register a locator. + */ +export interface LocatorDefinition

+ extends Partial> { + /** + * Unique ID of the locator. Should be constant and unique across Kibana. + */ + id: string; + + /** + * Returns a deep link, including location state, which can be used for + * navigation in Kibana. + * + * @param params Parameters from which to generate a Kibana location. + */ + getLocation(params: P): Promise; +} + +/** + * Public interface of a registered locator. + */ +export interface LocatorPublic

{ + /** + * Returns a relative URL to the client-side redirect endpoint using this + * locator. (This method is necessary for compatibility with URL generators.) + */ + getLocation(params: P): Promise; + + /** + * Navigate using the `core.application.navigateToApp()` method to a Kibana + * location generated by this locator. This method is available only on the + * browser. + */ + navigate(params: P, navigationParams?: LocatorNavigationParams): Promise; +} + +export interface LocatorNavigationParams { + replace?: boolean; +} + +/** + * This interface represents a location in Kibana to which one can navigate + * using the `core.application.navigateToApp()` method. + */ +export interface KibanaLocation { + /** + * Kibana application ID. + */ + app: string; + + /** + * A URL route within a Kibana application. + */ + route: string; + + /** + * A serializable location state object, which the app can use to determine + * what should be displayed on the screen. + */ + state: S; +} diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts new file mode 100644 index 0000000000000..0c3a0aabb750b --- /dev/null +++ b/src/plugins/share/common/url_service/url_service.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { LocatorClient, LocatorClientDependencies } from './locators'; + +export type UrlServiceDependencies = LocatorClientDependencies; + +/** + * Common URL Service client interface for server-side and client-side. + */ +export class UrlService { + /** + * Client to work with locators. + */ + locators: LocatorClient = new LocatorClient(this.deps); + + constructor(protected readonly deps: UrlServiceDependencies) {} +} diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 14d74e055cbd9..eb7c46cdaef86 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -18,6 +18,7 @@ import { UrlGeneratorsSetup, UrlGeneratorsStart, } from './url_generators/url_generator_service'; +import { UrlService } from '../common/url_service'; export interface ShareSetupDependencies { securityOss?: SecurityOssPluginSetup; @@ -27,16 +28,60 @@ export interface ShareStartDependencies { securityOss?: SecurityOssPluginStart; } +/** @public */ +export type SharePluginSetup = ShareMenuRegistrySetup & { + /** + * @deprecated + * + * URL Generators are deprecated use UrlService instead. + */ + urlGenerators: UrlGeneratorsSetup; + + /** + * Utilities to work with URL locators and short URLs. + */ + url: UrlService; +}; + +/** @public */ +export type SharePluginStart = ShareMenuManagerStart & { + /** + * @deprecated + * + * URL Generators are deprecated use UrlService instead. + */ + urlGenerators: UrlGeneratorsStart; + + /** + * Utilities to work with URL locators and short URLs. + */ + url: UrlService; +}; + export class SharePlugin implements Plugin { private readonly shareMenuRegistry = new ShareMenuRegistry(); private readonly shareContextMenu = new ShareMenuManager(); private readonly urlGeneratorsService = new UrlGeneratorsService(); + private url?: UrlService; public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup { core.application.register(createShortUrlRedirectApp(core, window.location)); + + this.url = new UrlService({ + navigate: async (location, { replace = false } = {}) => { + const [start] = await core.getStartServices(); + await start.application.navigateToApp(location.app, { + path: location.route, + state: location.state, + replace, + }); + }, + }); + return { ...this.shareMenuRegistry.setup(), urlGenerators: this.urlGeneratorsService.setup(core), + url: this.url, }; } @@ -48,16 +93,7 @@ export class SharePlugin implements Plugin { plugins.securityOss?.anonymousAccess ), urlGenerators: this.urlGeneratorsService.start(core), + url: this.url!, }; } } - -/** @public */ -export type SharePluginSetup = ShareMenuRegistrySetup & { - urlGenerators: UrlGeneratorsSetup; -}; - -/** @public */ -export type SharePluginStart = ShareMenuManagerStart & { - urlGenerators: UrlGeneratorsStart; -}; diff --git a/src/plugins/share/public/url_generators/README.md b/src/plugins/share/public/url_generators/README.md index 39ee5f2901e91..f948354aad959 100644 --- a/src/plugins/share/public/url_generators/README.md +++ b/src/plugins/share/public/url_generators/README.md @@ -1,3 +1,9 @@ +# URL Generators are deprecated + +__Below is documentation of URL Generators, which are now deprecated and will be removed in favor of URL locators in 7.14.__ + +--- + ## URL Generator Services Developers who maintain pages in Kibana that other developers may want to link to diff --git a/src/plugins/share/public/url_generators/url_generator_service.ts b/src/plugins/share/public/url_generators/url_generator_service.ts index 982f0692102df..5a8e7a1b5c17a 100644 --- a/src/plugins/share/public/url_generators/url_generator_service.ts +++ b/src/plugins/share/public/url_generators/url_generator_service.ts @@ -13,10 +13,20 @@ import { UrlGeneratorInternal } from './url_generator_internal'; import { UrlGeneratorContract } from './url_generator_contract'; export interface UrlGeneratorsStart { + /** + * @deprecated + * + * URL Generators are deprecated, use URL locators in UrlService instead. + */ getUrlGenerator: (urlGeneratorId: T) => UrlGeneratorContract; } export interface UrlGeneratorsSetup { + /** + * @deprecated + * + * URL Generators are deprecated, use URL locators in UrlService instead. + */ registerUrlGenerator: ( generator: UrlGeneratorsDefinition ) => UrlGeneratorContract; diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 744a4148215c3..6e3c68935f77b 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -12,11 +12,30 @@ import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { createRoutes } from './routes/create_routes'; import { url } from './saved_objects'; import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../common/constants'; +import { UrlService } from '../common/url_service'; + +/** @public */ +export interface SharePluginSetup { + url: UrlService; +} + +/** @public */ +export interface SharePluginStart { + url: UrlService; +} + +export class SharePlugin implements Plugin { + private url?: UrlService; -export class SharePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup) { + this.url = new UrlService({ + navigate: async () => { + throw new Error('Locator .navigate() does not work on server.'); + }, + }); + createRoutes(core, this.initializerContext.logger.get()); core.savedObjects.registerType(url); core.uiSettings.register({ @@ -41,10 +60,18 @@ export class SharePlugin implements Plugin { schema: schema.boolean(), }, }); + + return { + url: this.url, + }; } public start() { this.initializerContext.logger.get().debug('Starting plugin'); + + return { + url: this.url!, + }; } public stop() { From f10f25dd9377fab12d8e32069444e033cafdc25f Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Mon, 7 Jun 2021 13:19:01 +0200 Subject: [PATCH 4/7] Add link to advanced setting in Discover (#101154) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../top_nav/open_options_popover.tsx | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/plugins/discover/public/application/components/top_nav/open_options_popover.tsx b/src/plugins/discover/public/application/components/top_nav/open_options_popover.tsx index 280144d400216..e32ffa4a05de3 100644 --- a/src/plugins/discover/public/application/components/top_nav/open_options_popover.tsx +++ b/src/plugins/discover/public/application/components/top_nav/open_options_popover.tsx @@ -11,11 +11,21 @@ import ReactDOM from 'react-dom'; import { I18nStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButton, EuiText, EuiWrappingPopover, EuiCode } from '@elastic/eui'; +import { + EuiSpacer, + EuiButton, + EuiText, + EuiWrappingPopover, + EuiCode, + EuiHorizontalRule, + EuiButtonEmpty, + EuiTextAlign, +} from '@elastic/eui'; import { getServices } from '../../../kibana_services'; import './open_options_popover.scss'; import { DOC_TABLE_LEGACY } from '../../../../common'; +const container = document.createElement('div'); let isOpen = false; interface OptionsPopoverProps { @@ -77,11 +87,29 @@ export function OptionsPopover(props: OptionsPopoverProps) { defaultMessage: 'Get started', })} + + + + {i18n.translate('discover.openOptionsPopover.gotToAllSettings', { + defaultMessage: 'All Discover options', + })} + + ); } +function onClose() { + ReactDOM.unmountComponentAtNode(container); + document.body.removeChild(container); + isOpen = false; +} + export function openOptionsPopover({ I18nContext, anchorElement, @@ -90,17 +118,11 @@ export function openOptionsPopover({ anchorElement: HTMLElement; }) { if (isOpen) { + onClose(); return; } isOpen = true; - const container = document.createElement('div'); - const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - isOpen = false; - }; - document.body.appendChild(container); const element = ( From d1c7e982016e094222c24ce20a8df07c6a5fecf4 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Mon, 7 Jun 2021 14:30:36 +0300 Subject: [PATCH 5/7] [Discover] Fix header row of data grid in Firefox (#101374) * [Discover] add fix of row header in firefox from previous version * [Discover] add link comment to the issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/components/discover_grid/discover_grid.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss index 053b405b90acb..48b99458377ad 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -47,6 +47,9 @@ // We only truncate if the cell is not a control column. .euiDataGridHeader { + // This display property is temporary until https://github.com/elastic/eui/issues/4729 is resolved. + display: flex; + .euiDataGridHeaderCell__content { @include euiTextTruncate; overflow: hidden; From 1db14bd1deacae9ade86cf1eedda5fe137dab2a2 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 7 Jun 2021 13:38:49 +0200 Subject: [PATCH 6/7] Revert "[Reporting] ILM policy for managing reporting indices (#100130)" (#101358) This reverts commit 662fe7475738d90120342503e5a56016c2a5ee95. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/store/report_ilm_policy.ts | 18 ----- .../reporting/server/lib/store/store.test.ts | 39 ----------- .../reporting/server/lib/store/store.ts | 66 ++++--------------- x-pack/plugins/reporting/server/plugin.ts | 3 - 4 files changed, 12 insertions(+), 114 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts diff --git a/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts b/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts deleted file mode 100644 index f4cd69a0331d7..0000000000000 --- a/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PutLifecycleRequest } from '@elastic/elasticsearch/api/types'; - -export const reportingIlmPolicy: PutLifecycleRequest['body'] = { - policy: { - phases: { - hot: { - actions: {}, - }, - }, - }, -}; diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index fa35240dfc8fb..7f96433fcc6ce 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -7,7 +7,6 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { ElasticsearchClient } from 'src/core/server'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; import { createMockConfigSchema, @@ -17,8 +16,6 @@ import { import { Report, ReportDocument } from './report'; import { ReportingStore } from './store'; -const { createApiResponse } = elasticsearchServiceMock; - describe('ReportingStore', () => { const mockLogger = createMockLevelLogger(); let mockCore: ReportingCore; @@ -406,40 +403,4 @@ describe('ReportingStore', () => { ] `); }); - - describe('start', () => { - it('creates an ILM policy for managing reporting indices if there is not already one', async () => { - mockEsClient.ilm.getLifecycle.mockRejectedValueOnce(createApiResponse({ statusCode: 404 })); - mockEsClient.ilm.putLifecycle.mockResolvedValueOnce(createApiResponse()); - - const store = new ReportingStore(mockCore, mockLogger); - await store.start(); - - expect(mockEsClient.ilm.getLifecycle).toHaveBeenCalledWith({ policy: 'kibana-reporting' }); - expect(mockEsClient.ilm.putLifecycle.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "body": Object { - "policy": Object { - "phases": Object { - "hot": Object { - "actions": Object {}, - }, - }, - }, - }, - "policy": "kibana-reporting", - } - `); - }); - - it('does not create an ILM policy for managing reporting indices if one already exists', async () => { - mockEsClient.ilm.getLifecycle.mockResolvedValueOnce(createApiResponse()); - - const store = new ReportingStore(mockCore, mockLogger); - await store.start(); - - expect(mockEsClient.ilm.getLifecycle).toHaveBeenCalledWith({ policy: 'kibana-reporting' }); - expect(mockEsClient.ilm.putLifecycle).not.toHaveBeenCalled(); - }); - }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 9fb203fd5627a..fc7bd9c23d769 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -14,7 +14,6 @@ import { ReportTaskParams } from '../tasks'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; import { Report, ReportDocument, ReportSource } from './report'; -import { reportingIlmPolicy } from './report_ilm_policy'; /* * When searching for long-pending reports, we get a subset of fields @@ -72,22 +71,19 @@ export class ReportingStore { return exists; } + const indexSettings = { + number_of_shards: 1, + auto_expand_replicas: '0-1', + }; + const body = { + settings: indexSettings, + mappings: { + properties: mapping, + }, + }; + try { - await client.indices.create({ - index: indexName, - body: { - settings: { - number_of_shards: 1, - auto_expand_replicas: '0-1', - lifecycle: { - name: this.ilmPolicyName, - }, - }, - mappings: { - properties: mapping, - }, - }, - }); + await client.indices.create({ index: indexName, body }); return true; } catch (error) { @@ -134,44 +130,6 @@ export class ReportingStore { return client.indices.refresh({ index }); } - private readonly ilmPolicyName = 'kibana-reporting'; - - private async doesIlmPolicyExist(): Promise { - const client = await this.getClient(); - try { - await client.ilm.getLifecycle({ policy: this.ilmPolicyName }); - return true; - } catch (e) { - if (e.statusCode === 404) { - return false; - } - throw e; - } - } - - /** - * Function to be called during plugin start phase. This ensures the environment is correctly - * configured for storage of reports. - */ - public async start() { - const client = await this.getClient(); - try { - if (await this.doesIlmPolicyExist()) { - this.logger.debug(`Found ILM policy ${this.ilmPolicyName}; skipping creation.`); - return; - } - this.logger.info(`Creating ILM policy for managing reporting indices: ${this.ilmPolicyName}`); - await client.ilm.putLifecycle({ - policy: this.ilmPolicyName, - body: reportingIlmPolicy, - }); - } catch (e) { - this.logger.error('Error in start phase'); - this.logger.error(e.body.error); - throw e; - } - } - public async addReport(report: Report): Promise { let index = report._index; if (!index) { diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index dc0ddf27a53b3..4e7328cf18003 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -108,9 +108,6 @@ export class ReportingPlugin logger: this.logger, }); - // Note: this must be called after ReportingCore.pluginStart - await store.start(); - this.logger.debug('Start complete'); })().catch((e) => { this.logger.error(`Error in Reporting start, reporting may not function properly`); From 3930749f0ece88b8cd5fe3a67192d87741eb97a1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 7 Jun 2021 13:56:57 +0200 Subject: [PATCH 7/7] [Lens] Value in legend (#101353) --- .../legend_settings_popover.tsx | 35 +++++++++++++++++++ .../__snapshots__/to_expression.test.ts.snap | 3 ++ .../xy_visualization/expression.test.tsx | 31 ++++++++++++++++ .../public/xy_visualization/expression.tsx | 10 +++++- .../public/xy_visualization/to_expression.ts | 1 + .../lens/public/xy_visualization/types.ts | 2 ++ .../xy_visualization/xy_config_panel.tsx | 17 +++++++++ .../public/xy_visualization/xy_suggestions.ts | 1 + 8 files changed, 99 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx index 23d4858c26263..e86a81ba66203 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx @@ -45,6 +45,18 @@ export interface LegendSettingsPopoverProps { * Callback on nested switch status change */ onNestedLegendChange?: (event: EuiSwitchEvent) => void; + /** + * value in legend status + */ + valueInLegend?: boolean; + /** + * Callback on value in legend status change + */ + onValueInLegendChange?: (event: EuiSwitchEvent) => void; + /** + * If true, value in legend switch is rendered + */ + renderValueInLegendSwitch?: boolean; /** * Button group position */ @@ -91,6 +103,9 @@ export const LegendSettingsPopover: React.FunctionComponent {}, + valueInLegend, + onValueInLegendChange = () => {}, + renderValueInLegendSwitch, groupPosition = 'right', }) => { return ( @@ -161,6 +176,26 @@ export const LegendSettingsPopover: React.FunctionComponent )} + {renderValueInLegendSwitch && ( + + + + )} ); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 08b3393fafe48..ac8f089d46487 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -157,6 +157,9 @@ Object { "valueLabels": Array [ "hide", ], + "valuesInLegend": Array [ + false, + ], "xTitle": Array [ "", ], diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 3fab88248d4a5..ee1f66063ad1d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -265,6 +265,7 @@ const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({ position: Position.Top, }, valueLabels: 'hide', + valuesInLegend: false, axisTitlesVisibilitySettings: { type: 'lens_xy_axisTitlesVisibilityConfig', x: true, @@ -839,6 +840,36 @@ describe('xy_expression', () => { expect(component.find(Settings).prop('xDomain')).toEqual({ minInterval: 101 }); }); + test('disabled legend extra by default', () => { + const { data, args } = sampleArgs(); + const component = shallow(); + expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(false); + }); + + test('ignores legend extra for ordinal chart', () => { + const { data, args } = sampleArgs(); + const component = shallow( + + ); + expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(false); + }); + + test('shows legend extra for histogram chart', () => { + const { args } = sampleArgs(); + const component = shallow( + + ); + expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(true); + }); + test('it renders bar', () => { const { data, args } = sampleArgs(); const component = shallow( diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 9b203faee3a64..4cd2b55e8d424 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -209,6 +209,13 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Hide endzone markers for partial data', }), }, + valuesInLegend: { + types: ['boolean'], + default: false, + help: i18n.translate('xpack.lens.xyChart.valuesInLegend.help', { + defaultMessage: 'Show values in legend', + }), + }, }, fn(data: LensMultiTable, args: XYArgs) { return { @@ -365,6 +372,7 @@ export function XYChart({ hideEndzones, yLeftExtent, yRightExtent, + valuesInLegend, } = args; const chartTheme = chartsThemeService.useChartsTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); @@ -602,7 +610,6 @@ export function XYChart({ : legend.isVisible } legendPosition={legend.position} - showLegendExtra={false} theme={{ ...chartTheme, barSeriesStyle: { @@ -622,6 +629,7 @@ export function XYChart({ xDomain={xDomain} onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined} onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined} + showLegendExtra={isHistogramViz && valuesInLegend} /> { const columnToLabel = getColumnToLabelMap(layer, datasourceLayers[layer.layerId]); diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 531b034b53242..244898eda91ec 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -466,6 +466,7 @@ export interface XYArgs { curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; + valuesInLegend?: boolean; } export type XYCurveType = 'LINEAR' | 'CURVE_MONOTONE_X'; @@ -488,6 +489,7 @@ export interface XYState { curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; + valuesInLegend?: boolean; } export type State = XYState; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 48f0cacf75938..b3d1f8f062b73 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -227,6 +227,15 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp }); }; + const nonOrdinalXAxis = state?.layers.every( + (layer) => + !layer.xAccessor || + getScaleType( + props.frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor), + ScaleType.Linear + ) !== 'ordinal' + ); + // only allow changing endzone visibility if it could show up theoretically (if it's a time viz) const onChangeEndzoneVisiblity = state?.layers.every( (layer) => @@ -323,6 +332,14 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp legend: { ...state.legend, position: id as Position }, }); }} + renderValueInLegendSwitch={nonOrdinalXAxis} + valueInLegend={state?.valuesInLegend} + onValueInLegendChange={() => { + setState({ + ...state, + valuesInLegend: !state.valuesInLegend, + }); + }} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index aff33778258fe..a494d51f51681 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -529,6 +529,7 @@ function buildSuggestion({ yTitle: currentState?.yTitle, yRightTitle: currentState?.yRightTitle, hideEndzones: currentState?.hideEndzones, + valuesInLegend: currentState?.valuesInLegend, yLeftExtent: currentState?.yLeftExtent, yRightExtent: currentState?.yRightExtent, axisTitlesVisibilitySettings: currentState?.axisTitlesVisibilitySettings || {