From 6338a598f14733a343d7de288949be3de3a01454 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 7 Jun 2021 12:39:01 +0300 Subject: [PATCH 01/25] [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 02/25] 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 03/25] 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 04/25] [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 05/25] 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 06/25] [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 || { From a4b4da3674520aad4469b8ad1a390cbbf5561c80 Mon Sep 17 00:00:00 2001 From: Domenico Andreoli Date: Mon, 7 Jun 2021 14:41:33 +0200 Subject: [PATCH 07/25] [master] More precise alerts matching (#99820) * Split out test preparation and cleanup * Load data on the remote cluster * Update the rule to the new (remote) data --- .../apps/ccs/ccs_discover.js | 210 +++++++++++++----- 1 file changed, 150 insertions(+), 60 deletions(-) diff --git a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js index 7de23c2899b67..0713716ea6a77 100644 --- a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js +++ b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @@ -5,7 +5,12 @@ * 2.0. */ +import fs from 'fs'; import expect from '@kbn/expect'; +import { Client as EsClient } from '@elastic/elasticsearch'; +import { KbnClient } from '@kbn/test'; +import { EsArchiver } from '@kbn/es-archiver'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; export default ({ getService, getPageObjects }) => { describe('Cross cluster search test in discover', async () => { @@ -24,7 +29,6 @@ export default ({ getService, getPageObjects }) => { const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); const filterBar = getService('filterBar'); - const supertest = getService('supertest'); before(async () => { await browser.setWindowSize(1200, 800); @@ -98,8 +102,6 @@ export default ({ getService, getPageObjects }) => { ); await PageObjects.security.logout(); } - // visit app/security so to create .siem-signals-* as side effect - await PageObjects.common.navigateToApp('security', { insertTimestamp: false }); const url = await browser.getCurrentUrl(); log.debug(url); if (!url.includes('kibana')) { @@ -138,35 +140,6 @@ export default ({ getService, getPageObjects }) => { expect(patternName).to.be('*:makelogsๅทฅ็จ‹-*'); }); - it('create local siem signals index pattern', async () => { - log.debug('Add index pattern: .siem-signals-*'); - await supertest - .post('/api/index_patterns/index_pattern') - .set('kbn-xsrf', 'true') - .send({ - index_pattern: { - title: '.siem-signals-*', - }, - override: true, - }) - .expect(200); - }); - - it('create remote monitoring ES index pattern', async () => { - log.debug('Add index pattern: data:.monitoring-es-*'); - await supertest - .post('/api/index_patterns/index_pattern') - .set('kbn-xsrf', 'true') - .send({ - index_pattern: { - title: 'data:.monitoring-es-*', - timeFieldName: 'timestamp', - }, - override: true, - }) - .expect(200); - }); - it('local:makelogs(star) should discover data from the local cluster', async () => { await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); @@ -236,34 +209,151 @@ export default ({ getService, getPageObjects }) => { }); }); - it('should generate alerts based on remote events', async () => { - log.debug('Add detection rule type:shards on data:.monitoring-es-*'); - await supertest - .post('/api/detection_engine/rules') - .set('kbn-xsrf', 'true') - .send({ - description: 'This is the description of the rule', - risk_score: 17, - severity: 'low', - interval: '10s', - name: 'CCS_Detection_test', - type: 'query', - from: 'now-1d', - index: ['data:.monitoring-es-*'], - timestamp_override: 'timestamp', - query: 'type:shards', - language: 'kuery', - enabled: true, - }) - .expect(200); - - log.debug('Check if any alert got to .siem-signals-*'); - await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); - await PageObjects.discover.selectIndexPattern('.siem-signals-*'); - await retry.tryForTime(40000, async () => { - const hitCount = await PageObjects.discover.getHitCount(); - log.debug('### hit count = ' + hitCount); - expect(hitCount).to.be.greaterThan('0'); + describe('Detection engine', async function () { + const supertest = getService('supertest'); + const esSupertest = getService('esSupertest'); + const config = getService('config'); + + const esClient = new EsClient({ + ssl: { + ca: fs.readFileSync(CA_CERT_PATH, 'utf-8'), + }, + nodes: [process.env.TEST_ES_URLDATA], + requestTimeout: config.get('timeouts.esRequestTimeout'), + }); + + const kbnClient = new KbnClient({ + log, + url: process.env.TEST_KIBANA_URLDATA, + certificateAuthorities: config.get('servers.kibana.certificateAuthorities'), + uiSettingDefaults: kibanaServer.uiSettings, + importExportDir: config.get('kbnArchiver.directory'), + }); + + const esArchiver = new EsArchiver({ + log, + client: esClient, + kbnClient, + dataDir: config.get('esArchiver.directory'), + }); + + let signalsId; + let dataId; + let ruleId; + + before('Prepare .siem-signal-*', async function () { + log.info('Create index'); + // visit app/security so to create .siem-signals-* as side effect + await PageObjects.common.navigateToApp('security', { insertTimestamp: false }); + + log.info('Create index pattern'); + signalsId = await supertest + .post('/api/index_patterns/index_pattern') + .set('kbn-xsrf', 'true') + .send({ + index_pattern: { + title: '.siem-signals-*', + }, + override: true, + }) + .expect(200) + .then((res) => JSON.parse(res.text).index_pattern.id); + log.debug('id: ' + signalsId); + }); + + before('Prepare data:metricbeat-*', async function () { + log.info('Create index'); + await esArchiver.load('metricbeat'); + + log.info('Create index pattern'); + dataId = await supertest + .post('/api/index_patterns/index_pattern') + .set('kbn-xsrf', 'true') + .send({ + index_pattern: { + title: 'data:metricbeat-*', + }, + override: true, + }) + .expect(200) + .then((res) => JSON.parse(res.text).index_pattern.id); + log.debug('id: ' + dataId); + }); + + before('Add detection rule', async function () { + ruleId = await supertest + .post('/api/detection_engine/rules') + .set('kbn-xsrf', 'true') + .send({ + description: 'This is the description of the rule', + risk_score: 17, + severity: 'low', + interval: '10s', + name: 'CCS_Detection_test', + type: 'query', + from: 'now-1y', + index: ['data:metricbeat-*'], + query: '*:*', + language: 'kuery', + enabled: true, + }) + .expect(200) + .then((res) => JSON.parse(res.text).id); + log.debug('id: ' + ruleId); + }); + + after('Clean up detection rule', async function () { + if (ruleId !== undefined) { + log.debug('id: ' + ruleId); + await supertest + .delete('/api/detection_engine/rules?id=' + ruleId) + .set('kbn-xsrf', 'true') + .expect(200); + } + }); + + after('Clean up data:metricbeat-*', async function () { + if (dataId !== undefined) { + log.info('Delete index pattern'); + log.debug('id: ' + dataId); + await supertest + .delete('/api/index_patterns/index_pattern/' + dataId) + .set('kbn-xsrf', 'true') + .expect(200); + } + + log.info('Delete index'); + await esArchiver.unload('metricbeat'); + }); + + after('Clean up .siem-signal-*', async function () { + if (signalsId !== undefined) { + log.info('Delete index pattern: .siem-signals-*'); + log.debug('id: ' + signalsId); + await supertest + .delete('/api/index_patterns/index_pattern/' + signalsId) + .set('kbn-xsrf', 'true') + .expect(200); + } + + log.info('Delete index alias: .siem-signals-default'); + await esSupertest + .delete('/.siem-signals-default-000001/_alias/.siem-signals-default') + .expect(200); + + log.info('Delete index: .siem-signals-default-000001'); + await esSupertest.delete('/.siem-signals-default-000001').expect(200); + }); + + it('Should generate alerts based on remote events', async function () { + log.info('Check if any alert got to .siem-signals-*'); + await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); + await PageObjects.discover.selectIndexPattern('.siem-signals-*'); + await retry.tryForTime(30000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + log.debug('### hit count = ' + hitCount); + expect(hitCount).to.be('100'); + }); }); }); }); From 999faecd2c5c6b6e6013fbb20827847f2e02b703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 7 Jun 2021 15:28:01 +0200 Subject: [PATCH 08/25] [APM] Visual improvements for new APM layout with left navigation (#101360) --- .../public/app_links/redirect_app_link.tsx | 2 +- .../plugins/apm/public/application/index.tsx | 9 +- .../app/service_inventory/index.tsx | 22 ++- .../components/app/service_map/index.tsx | 42 ++++-- .../app/service_node_metrics/index.tsx | 106 +++++++-------- .../app/service_node_overview/index.tsx | 24 ++-- .../components/app/trace_overview/index.tsx | 11 +- .../components/routing/apm_route_config.tsx | 76 ++++++++--- .../public/components/routing/app_root.tsx | 32 ++--- .../route_handlers/agent_configuration.tsx | 65 --------- .../routing/templates/apm_main_template.tsx | 10 +- .../templates/apm_service_template.tsx | 127 ++++++++---------- .../templates/settings_template.test.tsx} | 6 +- .../templates/settings_template.tsx} | 4 +- .../public/components/shared/search_bar.tsx | 2 +- 15 files changed, 262 insertions(+), 276 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/routing/route_handlers/agent_configuration.tsx rename x-pack/plugins/apm/public/components/{app/Settings/Settings.test.tsx => routing/templates/settings_template.test.tsx} (89%) rename x-pack/plugins/apm/public/components/{app/Settings/index.tsx => routing/templates/settings_template.tsx} (96%) diff --git a/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx b/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx index d9ea8be9f5cdd..744a186a201ca 100644 --- a/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx +++ b/src/plugins/kibana_react/public/app_links/redirect_app_link.tsx @@ -12,7 +12,7 @@ import useObservable from 'react-use/lib/useObservable'; import { ApplicationStart } from 'src/core/public'; import { createNavigateToUrlClickHandler } from './click_handler'; -interface RedirectCrossAppLinksProps { +interface RedirectCrossAppLinksProps extends React.HTMLAttributes { application: ApplicationStart; className?: string; 'data-test-subj'?: string; diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index d5d77eea8c9c0..6924a8b9161b2 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -10,7 +10,11 @@ import ReactDOM from 'react-dom'; import 'react-vis/dist/style.css'; import type { ObservabilityRuleTypeRegistry } from '../../../observability/public'; import { ConfigSchema } from '../'; -import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; +import { + AppMountParameters, + CoreStart, + APP_WRAPPER_CLASS, +} from '../../../../../src/core/public'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; @@ -58,6 +62,9 @@ export const renderApp = ({ console.log('Error creating static index pattern', e); }); + // add .kbnAppWrappers class to root element + element.classList.add(APP_WRAPPER_CLASS); + ReactDOM.render( )} - - - } - /> - + + } + /> diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index 714228d58f962..df8438c5c80a4 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -5,7 +5,12 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; import React, { PropsWithChildren, ReactNode } from 'react'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useTrackPageview } from '../../../../../observability/public'; @@ -97,6 +102,10 @@ export function ServiceMap({ const { ref, height } = useRefDimensions(); + // Temporary hack to work around bottom padding introduced by EuiPage + const PADDING_BOTTOM = 24; + const heightWithPadding = height - PADDING_BOTTOM; + useTrackPageview({ app: 'apm', path: 'service_map' }); useTrackPageview({ app: 'apm', path: 'service_map', delay: 15000 }); @@ -137,20 +146,25 @@ export function ServiceMap({ return ( <> - -

- +
- - {serviceName && } - {status === FETCH_STATUS.LOADING && } - - -
+ + + {serviceName && } + {status === FETCH_STATUS.LOADING && } + + +
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 20b78b90e0378..5528bdaa40c41 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -39,13 +39,6 @@ const Truncate = euiStyled.span` ${truncate(px(unit * 12))} `; -const MetadataFlexGroup = euiStyled(EuiFlexGroup)` - border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; - padding: ${({ theme }) => - `${theme.eui.paddingSizes.m} 0 0 ${theme.eui.paddingSizes.m}`}; -`; - interface ServiceNodeMetricsProps { serviceName: string; serviceNodeName: string; @@ -117,55 +110,60 @@ export function ServiceNodeMetrics({ /> ) : ( - - - + + + + {serviceName} + + } + /> + + + + {host} + } - )} - title={ - - {serviceName} - - } - /> - - - - {host} - - } - /> - - - + + + + {containerId} + } - )} - title={ - - {containerId} - - } - /> - - + /> + + + )} {agentName && ( diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 69e5ea5a78ea1..1a432f90f1e3a 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiPanel, EuiToolTip } from '@elastic/eui'; +import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; @@ -142,18 +142,16 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { ]; return ( - - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index 0938456193dc0..bf60463255d64 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { EuiPanel } from '@elastic/eui'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -51,12 +50,10 @@ export function TraceOverview() { <> - - - + ); } diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index af62f4f235af7..09c25ee4557c5 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -14,17 +14,13 @@ import { toQuery } from '../shared/Links/url_helpers'; import { ErrorGroupDetails } from '../app/ErrorGroupDetails'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { ServiceNodeMetrics } from '../app/service_node_metrics'; -import { Settings } from '../app/Settings'; +import { SettingsTemplate } from './templates/settings_template'; import { AgentConfigurations } from '../app/Settings/AgentConfigurations'; import { AnomalyDetection } from '../app/Settings/anomaly_detection'; import { ApmIndices } from '../app/Settings/ApmIndices'; import { CustomizeUI } from '../app/Settings/CustomizeUI'; import { TraceLink } from '../app/TraceLink'; import { TransactionDetails } from '../app/transaction_details'; -import { - CreateAgentConfigurationRouteHandler, - EditAgentConfigurationRouteHandler, -} from './route_handlers/agent_configuration'; import { enableServiceOverview } from '../../../common/ui_settings_keys'; import { redirectTo } from './redirect_to'; import { ApmMainTemplate } from './templates/apm_main_template'; @@ -38,6 +34,8 @@ import { ServiceOverview } from '../app/service_overview'; import { TransactionOverview } from '../app/transaction_overview'; import { ServiceInventory } from '../app/service_inventory'; import { TraceOverview } from '../app/trace_overview'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { AgentConfigurationCreateEdit } from '../app/Settings/AgentConfigurations/AgentConfigurationCreateEdit'; // These component function definitions are used below with the `component` // property of the route definitions. @@ -222,9 +220,9 @@ function TransactionDetailsRouteView( function SettingsAgentConfigurationRouteView() { return ( - + - + ); } @@ -232,9 +230,9 @@ function SettingsAgentConfigurationRouteView() { function SettingsAnomalyDetectionRouteView() { return ( - + - + ); } @@ -242,9 +240,9 @@ function SettingsAnomalyDetectionRouteView() { function SettingsApmIndicesRouteView() { return ( - + - + ); } @@ -252,9 +250,57 @@ function SettingsApmIndicesRouteView() { function SettingsCustomizeUI() { return ( - + - + + + ); +} + +export function EditAgentConfigurationRouteView(props: RouteComponentProps) { + const { search } = props.history.location; + + // typescript complains because `pageStop` does not exist in `APMQueryParams` + // Going forward we should move away from globally declared query params and this is a first step + // @ts-expect-error + const { name, environment, pageStep } = toQuery(search); + + const res = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/settings/agent-configuration/view', + params: { query: { name, environment } }, + }); + }, + [name, environment] + ); + + return ( + + + + + + ); +} + +export function CreateAgentConfigurationRouteView(props: RouteComponentProps) { + const { search } = props.history.location; + + // Ignoring here because we specifically DO NOT want to add the query params to the global route handler + // @ts-expect-error + const { pageStep } = toQuery(search); + + return ( + + + + ); } @@ -339,14 +385,14 @@ export const apmRouteConfig: APMRouteDefinition[] = [ { exact: true, path: '/settings/agent-configuration/create', - component: CreateAgentConfigurationRouteHandler, + component: CreateAgentConfigurationRouteView, breadcrumb: CreateAgentConfigurationTitle, }, { exact: true, path: '/settings/agent-configuration/edit', breadcrumb: EditAgentConfigurationTitle, - component: EditAgentConfigurationRouteHandler, + component: EditAgentConfigurationRouteView, }, { exact: true, diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 9529a67210748..2bb387ae315ff 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; import { Route, Router, Switch } from 'react-router-dom'; import { DefaultTheme, ThemeProvider } from 'styled-components'; -import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { APP_WRAPPER_CLASS } from '../../../../../../src/core/public'; import { KibanaContextProvider, RedirectAppLinks, @@ -32,10 +32,6 @@ import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_con import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { apmRouteConfig } from './apm_route_config'; -const MainContainer = euiStyled.div` - height: 100%; -`; - export function ApmAppRoot({ apmPluginContextValue, pluginsStart, @@ -48,7 +44,12 @@ export function ApmAppRoot({ const i18nCore = core.i18n; return ( - + @@ -57,19 +58,14 @@ export function ApmAppRoot({ - - + - - - {apmRouteConfig.map((route, i) => ( - - ))} - - + + + {apmRouteConfig.map((route, i) => ( + + ))} + diff --git a/x-pack/plugins/apm/public/components/routing/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/routing/route_handlers/agent_configuration.tsx deleted file mode 100644 index 8e0a08603bc76..0000000000000 --- a/x-pack/plugins/apm/public/components/routing/route_handlers/agent_configuration.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; -import { useFetcher } from '../../../hooks/use_fetcher'; -import { toQuery } from '../../shared/Links/url_helpers'; -import { Settings } from '../../app/Settings'; -import { AgentConfigurationCreateEdit } from '../../app/Settings/AgentConfigurations/AgentConfigurationCreateEdit'; - -type EditAgentConfigurationRouteHandler = RouteComponentProps<{}>; - -export function EditAgentConfigurationRouteHandler( - props: EditAgentConfigurationRouteHandler -) { - const { search } = props.history.location; - - // typescript complains because `pageStop` does not exist in `APMQueryParams` - // Going forward we should move away from globally declared query params and this is a first step - // @ts-expect-error - const { name, environment, pageStep } = toQuery(search); - - const res = useFetcher( - (callApmApi) => { - return callApmApi({ - endpoint: 'GET /api/apm/settings/agent-configuration/view', - params: { query: { name, environment } }, - }); - }, - [name, environment] - ); - - return ( - - - - ); -} - -type CreateAgentConfigurationRouteHandlerProps = RouteComponentProps<{}>; - -export function CreateAgentConfigurationRouteHandler( - props: CreateAgentConfigurationRouteHandlerProps -) { - const { search } = props.history.location; - - // Ignoring here because we specifically DO NOT want to add the query params to the global route handler - // @ts-expect-error - const { pageStep } = toQuery(search); - - return ( - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index 0473e88c23d12..e917350f6024b 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui'; import React from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ApmPluginStartDeps } from '../../../plugin'; @@ -21,11 +22,14 @@ import { EnvironmentFilter } from '../../shared/EnvironmentFilter'; */ export function ApmMainTemplate({ pageTitle, + pageHeader, children, + ...pageTemplateProps }: { - pageTitle: React.ReactNode; + pageTitle?: React.ReactNode; + pageHeader?: EuiPageHeaderProps; children: React.ReactNode; -}) { +} & EuiPageTemplateProps) { const { services } = useKibana(); const ObservabilityPageTemplate = services.observability.navigation.PageTemplate; @@ -35,7 +39,9 @@ export function ApmMainTemplate({ pageHeader={{ pageTitle, rightSideItems: [], + ...pageHeader, }} + {...pageTemplateProps} > {children} diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx index 526d9eb3551d0..ab53052780ea9 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx @@ -10,9 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, + EuiPageHeaderProps, EuiTitle, - EuiTabs, - EuiTab, EuiBetaBadge, } from '@elastic/eui'; import { ApmMainTemplate } from './apm_main_template'; @@ -33,12 +32,10 @@ import { useUrlParams } from '../../../context/url_params_context/use_url_params import { Correlations } from '../../app/correlations'; import { SearchBar } from '../../shared/search_bar'; -interface Tab { - key: TabKey; - href: string; - text: React.ReactNode; +type Tab = NonNullable[0] & { + key: string; hidden?: boolean; -} +}; type TabKey = | 'errors' @@ -49,60 +46,63 @@ type TabKey = | 'profiling' | 'transactions'; -export function ApmServiceTemplate({ +interface Props { + children: React.ReactNode; + serviceName: string; + selectedTab: TabKey; + searchBarOptions?: React.ComponentProps; +} + +export function ApmServiceTemplate(props: Props) { + return ( + +