From 7fefdb1d8977c94a685876784ce6987b58f0c94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Tue, 6 Jul 2021 11:47:57 +0200 Subject: [PATCH 01/14] [DOCS] Changes docs link service link for ROC curve. (#104380) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/core/public/doc_links/doc_links_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6bb714e913838..f215c86d9d507 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -249,7 +249,7 @@ export class DocLinksService { customUrls: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`, dataFrameAnalytics: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`, featureImportance: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-feature-importance.html`, - outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-roc`, + outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`, classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-class-aucroc`, }, From 253a398db46b273945ac9bdfe4c63f5154fd2709 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 6 Jul 2021 12:09:16 +0200 Subject: [PATCH 02/14] [ML] Fix embeddable swim lane container to show a scrollbar on overflow (#104289) * [ML] fix legend * [ML] add extra div wrapper for overflow scroll --- .../explorer/swimlane_container.tsx | 138 +++++++++--------- 1 file changed, 71 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index d959328218a18..82f8a90fafb7d 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -40,7 +40,6 @@ import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants'; import { mlEscape } from '../util/string_utils'; import { FormattedTooltip, MlTooltipComponent } from '../components/chart_tooltip/chart_tooltip'; import { formatHumanReadableDateTime } from '../../../common/util/date_utils'; -import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils'; import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; @@ -62,6 +61,9 @@ declare global { } } +function getFormattedSeverityScore(score: number): string { + return String(parseInt(String(score), 10)); +} /** * Ignore insignificant resize, e.g. browser scrollbar appearance. */ @@ -122,7 +124,7 @@ const SwimLaneTooltip = (fieldName?: string): FC<{ values: TooltipValue[] }> => label: i18n.translate('xpack.ml.explorer.swimlane.maxAnomalyScoreLabel', { defaultMessage: 'Max anomaly score', }), - value: cell.formattedValue, + value: cell.formattedValue === '0' ? ' < 1' : cell.formattedValue, color: cell.color, // @ts-ignore seriesIdentifier: { @@ -408,73 +410,75 @@ export const SwimlaneContainer: FC = ({ grow={false} > <> -
- {showSwimlane && !isLoading && ( - - - - - - )} +
+
+ {showSwimlane && !isLoading && ( + + - {isLoading && ( - - + + )} + + {isLoading && ( + + + + )} + {!isLoading && !showSwimlane && ( + {noDataWarning}} /> - - )} - {!isLoading && !showSwimlane && ( - {noDataWarning}} - /> - )} + )} +
{swimlaneType === SWIMLANE_TYPE.OVERALL && showSwimlane && From 98942050c98c6efdceb14a8b87c64a63d4a239cb Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 6 Jul 2021 11:41:41 +0100 Subject: [PATCH 03/14] skip flaky suite (#104372) --- x-pack/test/functional/apps/discover/reporting.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 3eb66204df564..0b018b4428e1d 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -73,7 +73,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('Generate CSV: new search', () => { + // FLAKY: https://github.com/elastic/kibana/issues/104372 + describe.skip('Generate CSV: new search', () => { beforeEach(async () => { await kibanaServer.importExport.load(ecommerceSOPath); await PageObjects.common.navigateToApp('discover'); From 79608dcc9eee104372377e335fab5f10b0da6c88 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 6 Jul 2021 11:58:42 +0100 Subject: [PATCH 04/14] skip failing es promotion suite (#104409) --- test/functional/apps/discover/_discover.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index bb75b4441f880..245b895d75b3a 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -38,7 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - describe('query', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104409 + describe.skip('query', function () { const queryName1 = 'Query # 1'; it('should show correct time range string by timepicker', async function () { From ec5d3988654ab58ce3e2d4a23ee7f59530ba43bc Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 6 Jul 2021 13:01:51 +0200 Subject: [PATCH 05/14] Redirect endpoint (#103899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add redirect endpoint app * feat: 🎸 improve spinner design * feat: 🎸 implement basic version of redirect endpoint * feat: 🎸 render errors for user on the screen * feat: 🎸 improve error message display * feat: 🎸 improve error display * feat: 🎸 improve locator errors * feat: 🎸 improve errors * feat: 🎸 improve persistable state types * feat: 🎸 implement migrateToLatest function * feat: 🎸 migrate locator params to the latest in redirect endp * Update src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts * refactor: πŸ’‘ make Versioned state be an object * fix: πŸ› use new VersionedState interface in redirect endpoint * refactor: πŸ’‘ move parseSearchParams into a separate function * feat: 🎸 implement redirect URL formatter * feat: 🎸 export redirect URL parsing and formatting functions * refactor: πŸ’‘ use relative import * test: πŸ’ add example links through redirect endpoint * test: πŸ’ use updated VersionedState type * test: πŸ’ add redirect manager tests * feat: 🎸 add redirect endpoint app to common schema * chore: πŸ€– update telemetry schema Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- examples/locator_explorer/public/app.tsx | 21 +- .../collectors/application_usage/schema.ts | 1 + .../common/persistable_state/index.ts | 86 +-------- .../migrate_to_latest.test.ts | 152 +++++++++++++++ .../persistable_state/migrate_to_latest.ts | 30 +++ .../common/persistable_state/types.ts | 180 ++++++++++++++++++ src/plugins/share/common/mocks.ts | 9 + .../common/url_service/locators/locator.ts | 2 +- src/plugins/share/common/url_service/mocks.ts | 37 ++++ src/plugins/share/public/index.ts | 1 + src/plugins/share/public/mocks.ts | 9 + src/plugins/share/public/plugin.ts | 6 + src/plugins/share/public/url_service/index.ts | 9 + .../public/url_service/redirect/README.md | 18 ++ .../url_service/redirect/components/error.tsx | 53 ++++++ .../url_service/redirect/components/page.tsx | 46 +++++ .../redirect/components/spinner.tsx | 35 ++++ .../public/url_service/redirect/index.ts | 11 ++ .../redirect/redirect_manager.test.ts | 92 +++++++++ .../url_service/redirect/redirect_manager.ts | 95 +++++++++ .../public/url_service/redirect/render.ts | 19 ++ .../util/format_search_params.test.ts | 43 +++++ .../redirect/util/format_search_params.ts | 19 ++ .../redirect/util/parse_search_params.test.ts | 65 +++++++ .../redirect/util/parse_search_params.ts | 84 ++++++++ src/plugins/telemetry/schema/oss_plugins.json | 131 +++++++++++++ 26 files changed, 1168 insertions(+), 86 deletions(-) create mode 100644 src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts create mode 100644 src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts create mode 100644 src/plugins/kibana_utils/common/persistable_state/types.ts create mode 100644 src/plugins/share/common/mocks.ts create mode 100644 src/plugins/share/common/url_service/mocks.ts create mode 100644 src/plugins/share/public/mocks.ts create mode 100644 src/plugins/share/public/url_service/index.ts create mode 100644 src/plugins/share/public/url_service/redirect/README.md create mode 100644 src/plugins/share/public/url_service/redirect/components/error.tsx create mode 100644 src/plugins/share/public/url_service/redirect/components/page.tsx create mode 100644 src/plugins/share/public/url_service/redirect/components/spinner.tsx create mode 100644 src/plugins/share/public/url_service/redirect/index.ts create mode 100644 src/plugins/share/public/url_service/redirect/redirect_manager.test.ts create mode 100644 src/plugins/share/public/url_service/redirect/redirect_manager.ts create mode 100644 src/plugins/share/public/url_service/redirect/render.ts create mode 100644 src/plugins/share/public/url_service/redirect/util/format_search_params.test.ts create mode 100644 src/plugins/share/public/url_service/redirect/util/format_search_params.ts create mode 100644 src/plugins/share/public/url_service/redirect/util/parse_search_params.test.ts create mode 100644 src/plugins/share/public/url_service/redirect/util/parse_search_params.ts diff --git a/examples/locator_explorer/public/app.tsx b/examples/locator_explorer/public/app.tsx index 440e16302dff9..8e38c097a847e 100644 --- a/examples/locator_explorer/public/app.tsx +++ b/examples/locator_explorer/public/app.tsx @@ -19,7 +19,7 @@ import { EuiFieldText } from '@elastic/eui'; import { EuiPageHeader } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; import { AppMountParameters } from '../../../src/core/public'; -import { SharePluginSetup } from '../../../src/plugins/share/public'; +import { formatSearchParams, SharePluginSetup } from '../../../src/plugins/share/public'; import { HelloLocatorV1Params, HelloLocatorV2Params, @@ -34,6 +34,7 @@ interface MigratedLink { linkText: string; link: string; version: string; + params: HelloLocatorV1Params | HelloLocatorV2Params; } const ActionsExplorer = ({ share }: Props) => { @@ -93,6 +94,7 @@ const ActionsExplorer = ({ share }: Props) => { linkText: savedLink.linkText, link, version: savedLink.version, + params: savedLink.params, } as MigratedLink; }) ); @@ -157,7 +159,24 @@ const ActionsExplorer = ({ share }: Props) => { target="_blank" > {link.linkText} + {' '} + ( + + through redirect app + )
)) diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 65857f02c883d..54a3fe9e4399c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -129,6 +129,7 @@ export const applicationUsageSchema = { error: commonSchema, status: commonSchema, kibanaOverview: commonSchema, + r: commonSchema, // X-Pack apm: commonSchema, diff --git a/src/plugins/kibana_utils/common/persistable_state/index.ts b/src/plugins/kibana_utils/common/persistable_state/index.ts index 809cb15c3e960..18f59186f6183 100644 --- a/src/plugins/kibana_utils/common/persistable_state/index.ts +++ b/src/plugins/kibana_utils/common/persistable_state/index.ts @@ -6,87 +6,5 @@ * Side Public License, v 1. */ -import { SavedObjectReference } from '../../../../core/types'; - -export type SerializableValue = string | number | boolean | null | undefined | SerializableState; -export type Serializable = SerializableValue | SerializableValue[]; - -export type SerializableState = { - [key: string]: Serializable; -}; - -export type MigrateFunction< - FromVersion extends SerializableState = SerializableState, - ToVersion extends SerializableState = SerializableState -> = (state: FromVersion) => ToVersion; - -export type MigrateFunctionsObject = { - [key: string]: MigrateFunction; -}; - -export interface PersistableStateService

{ - /** - * function to extract telemetry information - * @param state - * @param collector - */ - telemetry: (state: P, collector: Record) => Record; - /** - * inject function receives state and a list of references and should return state with references injected - * default is identity function - * @param state - * @param references - */ - inject: (state: P, references: SavedObjectReference[]) => P; - /** - * extract function receives state and should return state with references extracted and array of references - * default returns same state with empty reference array - * @param state - */ - extract: (state: P) => { state: P; references: SavedObjectReference[] }; - - /** - * migrateToLatest function receives state of older version and should migrate to the latest version - * @param state - * @param version - */ - migrateToLatest?: (state: SerializableState, version: string) => P; - - /** - * migrate function runs the specified migration - * @param state - * @param version - */ - migrate: (state: SerializableState, version: string) => SerializableState; -} - -export interface PersistableState

{ - /** - * function to extract telemetry information - * @param state - * @param collector - */ - telemetry: (state: P, collector: Record) => Record; - /** - * inject function receives state and a list of references and should return state with references injected - * default is identity function - * @param state - * @param references - */ - inject: (state: P, references: SavedObjectReference[]) => P; - /** - * extract function receives state and should return state with references extracted and array of references - * default returns same state with empty reference array - * @param state - */ - extract: (state: P) => { state: P; references: SavedObjectReference[] }; - - /** - * list of all migrations per semver - */ - migrations: MigrateFunctionsObject; -} - -export type PersistableStateDefinition

= Partial< - PersistableState

->; +export * from './types'; +export { migrateToLatest } from './migrate_to_latest'; diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts new file mode 100644 index 0000000000000..2ae376e787d2f --- /dev/null +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { SerializableState, MigrateFunction } from './types'; +import { migrateToLatest } from './migrate_to_latest'; + +interface StateV1 extends SerializableState { + name: string; +} + +interface StateV2 extends SerializableState { + firstName: string; + lastName: string; +} + +interface StateV3 extends SerializableState { + firstName: string; + lastName: string; + isAdmin: boolean; + age: number; +} + +const migrationV2: MigrateFunction = ({ name }) => { + return { + firstName: name, + lastName: '', + }; +}; + +const migrationV3: MigrateFunction = ({ firstName, lastName }) => { + return { + firstName, + lastName, + isAdmin: false, + age: 0, + }; +}; + +test('returns the same object if there are no migrations to be applied', () => { + const migrated = migrateToLatest( + {}, + { + state: { name: 'Foo' }, + version: '0.0.1', + } + ); + + expect(migrated).toEqual({ + state: { name: 'Foo' }, + version: '0.0.1', + }); +}); + +test('applies a single migration', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '0.0.2': (migrationV2 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '0.0.1', + } + ); + + expect(newState).toEqual({ + firstName: 'Foo', + lastName: '', + }); + expect(newVersion).toEqual('0.0.2'); +}); + +test('does not apply migration if it has the same version as state', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '0.0.54': (migrationV2 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '0.0.54', + } + ); + + expect(newState).toEqual({ + name: 'Foo', + }); + expect(newVersion).toEqual('0.0.54'); +}); + +test('does not apply migration if it has lower version', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '0.2.2': (migrationV2 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '0.3.1', + } + ); + + expect(newState).toEqual({ + name: 'Foo', + }); + expect(newVersion).toEqual('0.3.1'); +}); + +test('applies two migrations consecutively', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '7.14.0': (migrationV2 as unknown) as MigrateFunction, + '7.14.2': (migrationV3 as unknown) as MigrateFunction, + }, + { + state: { name: 'Foo' }, + version: '7.13.4', + } + ); + + expect(newState).toEqual({ + firstName: 'Foo', + lastName: '', + isAdmin: false, + age: 0, + }); + expect(newVersion).toEqual('7.14.2'); +}); + +test('applies only migrations which are have higher semver version', () => { + const { state: newState, version: newVersion } = migrateToLatest( + { + '7.14.0': (migrationV2 as unknown) as MigrateFunction, // not applied + '7.14.1': (() => ({})) as MigrateFunction, // not applied + '7.14.2': (migrationV3 as unknown) as MigrateFunction, + }, + { + state: { firstName: 'FooBar', lastName: 'Baz' }, + version: '7.14.1', + } + ); + + expect(newState).toEqual({ + firstName: 'FooBar', + lastName: 'Baz', + isAdmin: false, + age: 0, + }); + expect(newVersion).toEqual('7.14.2'); +}); diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts new file mode 100644 index 0000000000000..c16392164e3e4 --- /dev/null +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { compare } from 'semver'; +import { SerializableState, VersionedState, MigrateFunctionsObject } from './types'; + +export function migrateToLatest( + migrations: MigrateFunctionsObject, + { state, version: oldVersion }: VersionedState +): VersionedState { + const versions = Object.keys(migrations || {}) + .filter((v) => compare(v, oldVersion) > 0) + .sort(compare); + + if (!versions.length) return { state, version: oldVersion } as VersionedState; + + for (const version of versions) { + state = migrations[version]!(state); + } + + return { + state: state as S, + version: versions[versions.length - 1], + }; +} diff --git a/src/plugins/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts new file mode 100644 index 0000000000000..f7168b46e7fca --- /dev/null +++ b/src/plugins/kibana_utils/common/persistable_state/types.ts @@ -0,0 +1,180 @@ +/* + * 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 { SavedObjectReference } from '../../../../core/types'; + +/** + * Serializable state is something is a POJO JavaScript object that can be + * serialized to a JSON string. + */ +export type SerializableState = { + [key: string]: Serializable; +}; +export type SerializableValue = string | number | boolean | null | undefined | SerializableState; +export type Serializable = SerializableValue | SerializableValue[]; + +/** + * Versioned state is a POJO JavaScript object that can be serialized to JSON, + * and which also contains the version information. The version is stored in + * semver format and corresponds to the Kibana release version when the object + * was created. The version can be used to apply migrations to the object. + * + * For example: + * + * ```ts + * const obj: VersionedState<{ dashboardId: string }> = { + * version: '7.14.0', + * state: { + * dashboardId: '123', + * }, + * }; + * ``` + */ +export interface VersionedState { + version: string; + state: S; +} + +/** + * Persistable state interface can be implemented by something that persists + * (stores) state, for example, in a saved object. Once implemented that thing + * will gain ability to "extract" and "inject" saved object references, which + * are necessary for various saved object tasks, such as export. It will also be + * able to do state migrations across Kibana versions, if the shape of the state + * would change over time. + * + * @todo Maybe rename it to `PersistableStateItem`? + */ +export interface PersistableState

{ + /** + * Function which reports telemetry information. This function is essentially + * a "reducer" - it receives the existing "stats" object and returns an + * updated version of the "stats" object. + * + * @param state The persistable state serializable state object. + * @param stats Stats object containing the stats which were already + * collected. This `stats` object shall not be mutated in-line. + * @returns A new stats object augmented with new telemetry information. + */ + telemetry: (state: P, stats: Record) => Record; + + /** + * A function which receives state and a list of references and should return + * back the state with references injected. The default is an identity + * function. + * + * @param state The persistable state serializable state object. + * @param references List of saved object references. + * @returns Persistable state object with references injected. + */ + inject: (state: P, references: SavedObjectReference[]) => P; + + /** + * A function which receives state and should return the state with references + * extracted and an array of the extracted references. The default case could + * simply return the same state with an empty array of references. + * + * @param state The persistable state serializable state object. + * @returns Persistable state object with references extracted and a list of + * references. + */ + extract: (state: P) => { state: P; references: SavedObjectReference[] }; + + /** + * A list of migration functions, which migrate the persistable state + * serializable object to the next version. Migration functions should are + * keyed by the Kibana version using semver, where the version indicates to + * which version the state will be migrated to. + */ + migrations: MigrateFunctionsObject; +} + +/** + * Collection of migrations that a given type of persistable state object has + * accumulated over time. Migration functions are keyed using semver version + * of Kibana releases. + */ +export type MigrateFunctionsObject = { [semver: string]: MigrateFunction }; +export type MigrateFunction< + FromVersion extends SerializableState = SerializableState, + ToVersion extends SerializableState = SerializableState +> = (state: FromVersion) => ToVersion; + +/** + * @todo Shall we remove this? + */ +export type PersistableStateDefinition

= Partial< + PersistableState

+>; + +/** + * @todo Add description. + */ +export interface PersistableStateService

{ + /** + * Function which reports telemetry information. This function is essentially + * a "reducer" - it receives the existing "stats" object and returns an + * updated version of the "stats" object. + * + * @param state The persistable state serializable state object. + * @param stats Stats object containing the stats which were already + * collected. This `stats` object shall not be mutated in-line. + * @returns A new stats object augmented with new telemetry information. + */ + telemetry(state: P, collector: Record): Record; + + /** + * A function which receives state and a list of references and should return + * back the state with references injected. The default is an identity + * function. + * + * @param state The persistable state serializable state object. + * @param references List of saved object references. + * @returns Persistable state object with references injected. + */ + inject(state: P, references: SavedObjectReference[]): P; + + /** + * A function which receives state and should return the state with references + * extracted and an array of the extracted references. The default case could + * simply return the same state with an empty array of references. + * + * @param state The persistable state serializable state object. + * @returns Persistable state object with references extracted and a list of + * references. + */ + extract(state: P): { state: P; references: SavedObjectReference[] }; + + /** + * Migrate function runs a specified migration of a {@link PersistableState} + * item. + * + * When using this method it is up to consumer to make sure that the + * migration function are executed in the right semver order. To avoid such + * potentially error prone complexity, prefer using `migrateToLatest` method + * instead. + * + * @param state The old persistable state serializable state object, which + * needs a migration. + * @param version Semver version of the migration to execute. + * @returns Persistable state object updated with the specified migration + * applied to it. + */ + migrate(state: SerializableState, version: string): SerializableState; + + /** + * A function which receives the state of an older object and version and + * should migrate the state of the object to the latest possible version using + * the `.migrations` dictionary provided on a {@link PersistableState} item. + * + * @param state The persistable state serializable state object. + * @param version Current semver version of the `state`. + * @returns A serializable state object migrated to the latest state. + */ + migrateToLatest?: (state: VersionedState) => VersionedState

; +} diff --git a/src/plugins/share/common/mocks.ts b/src/plugins/share/common/mocks.ts new file mode 100644 index 0000000000000..6768c1aff810a --- /dev/null +++ b/src/plugins/share/common/mocks.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './url_service/mocks'; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index 680fb2231fc48..bae57b6d8a31d 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -30,7 +30,7 @@ export interface LocatorDependencies { getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } -export class Locator

implements PersistableState

, LocatorPublic

{ +export class Locator

implements LocatorPublic

{ public readonly migrations: PersistableState

['migrations']; constructor( diff --git a/src/plugins/share/common/url_service/mocks.ts b/src/plugins/share/common/url_service/mocks.ts new file mode 100644 index 0000000000000..be86cfe401713 --- /dev/null +++ b/src/plugins/share/common/url_service/mocks.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable max-classes-per-file */ + +import type { LocatorDefinition, KibanaLocation } from '.'; +import { UrlService } from '.'; + +export class MockUrlService extends UrlService { + constructor() { + super({ + navigate: async () => {}, + getUrl: async ({ app, path }, { absolute }) => { + return `${absolute ? 'https://example.com' : ''}/app/${app}${path}`; + }, + }); + } +} + +export class MockLocatorDefinition implements LocatorDefinition { + constructor(public readonly id: string) {} + + public readonly getLocation = async (): Promise => { + return { + app: 'test', + path: '/test', + state: { + foo: 'bar', + }, + }; + }; +} diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 5ee3156534c5e..1f999b59ddb61 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -9,6 +9,7 @@ export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; export { LocatorDefinition, LocatorPublic, KibanaLocation } from '../common/url_service'; +export { parseSearchParams, formatSearchParams } from './url_service'; export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; diff --git a/src/plugins/share/public/mocks.ts b/src/plugins/share/public/mocks.ts new file mode 100644 index 0000000000000..eb9c6d0d10906 --- /dev/null +++ b/src/plugins/share/public/mocks.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from '../common/mocks'; diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 893108b56bcfa..adc28556d7a3c 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -19,6 +19,7 @@ import { UrlGeneratorsStart, } from './url_generators/url_generator_service'; import { UrlService } from '../common/url_service'; +import { RedirectManager } from './url_service'; export interface ShareSetupDependencies { securityOss?: SecurityOssPluginSetup; @@ -86,6 +87,11 @@ export class SharePlugin implements Plugin { }, }); + const redirectManager = new RedirectManager({ + url: this.url, + }); + redirectManager.registerRedirectApp(core); + return { ...this.shareMenuRegistry.setup(), urlGenerators: this.urlGeneratorsService.setup(core), diff --git a/src/plugins/share/public/url_service/index.ts b/src/plugins/share/public/url_service/index.ts new file mode 100644 index 0000000000000..8fa88e9c570bd --- /dev/null +++ b/src/plugins/share/public/url_service/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './redirect'; diff --git a/src/plugins/share/public/url_service/redirect/README.md b/src/plugins/share/public/url_service/redirect/README.md new file mode 100644 index 0000000000000..cd31f2b80099b --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/README.md @@ -0,0 +1,18 @@ +# Redirect endpoint + +This folder contains implementation of *the Redirect Endpoint*. The Redirect +Endpoint receives parameters of a locator and then "redirects" the user using +navigation without page refresh to the location targeted by the locator. While +using the locator, it is also possible to set the *location state* of the +target page. Location state is a serializable object which can be passed to +the destination app while navigating without a page reload. + +``` +/app/r?l=MY_LOCATOR&v=7.14.0&p=(dashboardId:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) +``` + +For example: + +``` +/app/r?l=DISCOVER_APP_LOCATOR&v=7.14.0&p={%22indexPatternId%22:%22d3d7af60-4c81-11e8-b3d7-01146121b73d%22} +``` diff --git a/src/plugins/share/public/url_service/redirect/components/error.tsx b/src/plugins/share/public/url_service/redirect/components/error.tsx new file mode 100644 index 0000000000000..716848427c638 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/components/error.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as React from 'react'; +import { + EuiEmptyPrompt, + EuiCallOut, + EuiCodeBlock, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const defaultTitle = i18n.translate('share.urlService.redirect.components.Error.title', { + defaultMessage: 'Redirection error', + description: + 'Title displayed to user in redirect endpoint when redirection cannot be performed successfully.', +}); + +export interface ErrorProps { + title?: string; + error: Error; +} + +export const Error: React.FC = ({ title = defaultTitle, error }) => { + return ( + {title}} + body={ + + + + {error.message} + + + + + {error.stack ? error.stack : ''} + + + } + /> + ); +}; diff --git a/src/plugins/share/public/url_service/redirect/components/page.tsx b/src/plugins/share/public/url_service/redirect/components/page.tsx new file mode 100644 index 0000000000000..805213b73fdd0 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/components/page.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 * as React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { EuiPageTemplate } from '@elastic/eui'; +import { Error } from './error'; +import { RedirectManager } from '../redirect_manager'; +import { Spinner } from './spinner'; + +export interface PageProps { + manager: Pick; +} + +export const Page: React.FC = ({ manager }) => { + const error = useObservable(manager.error$); + + if (error) { + return ( + + + + ); + } + + return ( + + + + ); +}; diff --git a/src/plugins/share/public/url_service/redirect/components/spinner.tsx b/src/plugins/share/public/url_service/redirect/components/spinner.tsx new file mode 100644 index 0000000000000..a70ae5eb096af --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/components/spinner.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 * as React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const text = i18n.translate('share.urlService.redirect.components.Spinner.label', { + defaultMessage: 'Redirecting…', + description: 'Redirect endpoint spinner label.', +}); + +export const Spinner: React.FC = () => { + return ( + + + + + + + + + {text} + + + + + + ); +}; diff --git a/src/plugins/share/public/url_service/redirect/index.ts b/src/plugins/share/public/url_service/redirect/index.ts new file mode 100644 index 0000000000000..8dbc5f4e0ab1c --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/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 './redirect_manager'; +export { formatSearchParams } from './util/format_search_params'; +export { parseSearchParams } from './util/parse_search_params'; diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.test.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.test.ts new file mode 100644 index 0000000000000..f610268f529bc --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { RedirectManager } from './redirect_manager'; +import { MockUrlService } from '../../mocks'; +import { MigrateFunction } from 'src/plugins/kibana_utils/common'; + +const setup = () => { + const url = new MockUrlService(); + const locator = url.locators.create({ + id: 'TEST_LOCATOR', + getLocation: async () => { + return { + app: '', + path: '', + state: {}, + }; + }, + migrations: { + '0.0.2': ((({ num }: { num: number }) => ({ num: num * 2 })) as unknown) as MigrateFunction, + }, + }); + const manager = new RedirectManager({ + url, + }); + + return { + url, + locator, + manager, + }; +}; + +describe('on page mount', () => { + test('execute locator "navigate" method', async () => { + const { locator, manager } = setup(); + const spy = jest.spyOn(locator, 'navigate'); + + expect(spy).toHaveBeenCalledTimes(0); + manager.onMount(`l=TEST_LOCATOR&v=0.0.3&p=${encodeURIComponent(JSON.stringify({}))}`); + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('passes arguments provided in URL to locator "navigate" method', async () => { + const { locator, manager } = setup(); + const spy = jest.spyOn(locator, 'navigate'); + + manager.onMount( + `l=TEST_LOCATOR&v=0.0.3&p=${encodeURIComponent( + JSON.stringify({ + foo: 'bar', + }) + )}` + ); + expect(spy).toHaveBeenCalledWith({ + foo: 'bar', + }); + }); + + test('migrates parameters on-the-fly to the latest version', async () => { + const { locator, manager } = setup(); + const spy = jest.spyOn(locator, 'navigate'); + + manager.onMount( + `l=TEST_LOCATOR&v=0.0.1&p=${encodeURIComponent( + JSON.stringify({ + num: 1, + }) + )}` + ); + expect(spy).toHaveBeenCalledWith({ + num: 2, + }); + }); + + test('throws if locator does not exist', async () => { + const { manager } = setup(); + + expect(() => + manager.onMount( + `l=TEST_LOCATOR_WHICH_DOES_NOT_EXIST&v=0.0.3&p=${encodeURIComponent(JSON.stringify({}))}` + ) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator [ID = TEST_LOCATOR_WHICH_DOES_NOT_EXIST] does not exist."` + ); + }); +}); diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts new file mode 100644 index 0000000000000..6148249f5a047 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts @@ -0,0 +1,95 @@ +/* + * 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 { CoreSetup } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { migrateToLatest } from '../../../../kibana_utils/common'; +import type { SerializableState } from '../../../../kibana_utils/common'; +import type { UrlService } from '../../../common/url_service'; +import { render } from './render'; +import { parseSearchParams } from './util/parse_search_params'; + +export interface RedirectOptions { + /** Locator ID. */ + id: string; + + /** Kibana version when locator params where generated. */ + version: string; + + /** Locator params. */ + params: unknown & SerializableState; +} + +export interface RedirectManagerDependencies { + url: UrlService; +} + +export class RedirectManager { + public readonly error$ = new BehaviorSubject(null); + + constructor(public readonly deps: RedirectManagerDependencies) {} + + public registerRedirectApp(core: CoreSetup) { + core.application.register({ + id: 'r', + title: 'Redirect endpoint', + chromeless: true, + mount: (params) => { + const unmount = render(params.element, { manager: this }); + this.onMount(params.history.location.search); + return () => { + unmount(); + }; + }, + }); + } + + public onMount(urlLocationSearch: string) { + const options = this.parseSearchParams(urlLocationSearch); + const locator = this.deps.url.locators.get(options.id); + + if (!locator) { + const message = i18n.translate('share.urlService.redirect.RedirectManager.locatorNotFound', { + defaultMessage: 'Locator [ID = {id}] does not exist.', + values: { + id: options.id, + }, + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because locator does not exist.', + }); + const error = new Error(message); + this.error$.next(error); + throw error; + } + + const { state: migratedParams } = migrateToLatest(locator.migrations, { + state: options.params, + version: options.version, + }); + + locator + .navigate(migratedParams) + .then() + .catch((error) => { + // eslint-disable-next-line no-console + console.log('Redirect endpoint failed to execute locator redirect.'); + // eslint-disable-next-line no-console + console.error(error); + }); + } + + protected parseSearchParams(urlLocationSearch: string): RedirectOptions { + try { + return parseSearchParams(urlLocationSearch); + } catch (error) { + this.error$.next(error); + throw error; + } + } +} diff --git a/src/plugins/share/public/url_service/redirect/render.ts b/src/plugins/share/public/url_service/redirect/render.ts new file mode 100644 index 0000000000000..2b9c3a50758e4 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/render.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { Page, PageProps } from './components/page'; + +export const render = (container: HTMLElement, props: PageProps) => { + ReactDOM.render(React.createElement(Page, props), container); + + return () => { + ReactDOM.unmountComponentAtNode(container); + }; +}; diff --git a/src/plugins/share/public/url_service/redirect/util/format_search_params.test.ts b/src/plugins/share/public/url_service/redirect/util/format_search_params.test.ts new file mode 100644 index 0000000000000..f8d8d6a6295d9 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/format_search_params.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { formatSearchParams } from './format_search_params'; +import { parseSearchParams } from './parse_search_params'; + +test('can format typical locator settings as URL path search params', () => { + const search = formatSearchParams({ + id: 'LOCATOR_ID', + version: '7.21.3', + params: { + dashboardId: '123', + mode: 'edit', + }, + }); + + expect(search.get('l')).toBe('LOCATOR_ID'); + expect(search.get('v')).toBe('7.21.3'); + expect(JSON.parse(search.get('p')!)).toEqual({ + dashboardId: '123', + mode: 'edit', + }); +}); + +test('can format and then parse redirect options', () => { + const options = { + id: 'LOCATOR_ID', + version: '7.21.3', + params: { + dashboardId: '123', + mode: 'edit', + }, + }; + const formatted = formatSearchParams(options); + const parsed = parseSearchParams(formatted.toString()); + + expect(parsed).toEqual(options); +}); diff --git a/src/plugins/share/public/url_service/redirect/util/format_search_params.ts b/src/plugins/share/public/url_service/redirect/util/format_search_params.ts new file mode 100644 index 0000000000000..12c6424182a87 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/format_search_params.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { RedirectOptions } from '../redirect_manager'; + +export function formatSearchParams(opts: RedirectOptions): URLSearchParams { + const searchParams = new URLSearchParams(); + + searchParams.set('l', opts.id); + searchParams.set('v', opts.version); + searchParams.set('p', JSON.stringify(opts.params)); + + return searchParams; +} diff --git a/src/plugins/share/public/url_service/redirect/util/parse_search_params.test.ts b/src/plugins/share/public/url_service/redirect/util/parse_search_params.test.ts new file mode 100644 index 0000000000000..418e21cfd4053 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/parse_search_params.test.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 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 { parseSearchParams } from './parse_search_params'; + +test('parses a well constructed URL path search part', () => { + const res = parseSearchParams(`?l=LOCATOR&v=0.0.0&p=${encodeURIComponent('{"foo":"bar"}')}`); + + expect(res).toEqual({ + id: 'LOCATOR', + version: '0.0.0', + params: { + foo: 'bar', + }, + }); +}); + +test('throws on missing locator ID', () => { + expect(() => + parseSearchParams(`?v=0.0.0&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator ID not specified. Specify \\"l\\" search parameter in the URL, which should be an existing locator ID."` + ); + + expect(() => + parseSearchParams(`?l=&v=0.0.0&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator ID not specified. Specify \\"l\\" search parameter in the URL, which should be an existing locator ID."` + ); +}); + +test('throws on missing version', () => { + expect(() => + parseSearchParams(`?l=LOCATOR&v=&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator params version not specified. Specify \\"v\\" search parameter in the URL, which should be the release version of Kibana when locator params were generated."` + ); + + expect(() => + parseSearchParams(`?l=LOCATOR&p=${encodeURIComponent('{"foo":"bar"}')}`) + ).toThrowErrorMatchingInlineSnapshot( + `"Locator params version not specified. Specify \\"v\\" search parameter in the URL, which should be the release version of Kibana when locator params were generated."` + ); +}); + +test('throws on missing params', () => { + expect(() => parseSearchParams(`?l=LOCATOR&v=1.1.1`)).toThrowErrorMatchingInlineSnapshot( + `"Locator params not specified. Specify \\"p\\" search parameter in the URL, which should be JSON serialized object of locator params."` + ); + + expect(() => parseSearchParams(`?l=LOCATOR&v=1.1.1&p=`)).toThrowErrorMatchingInlineSnapshot( + `"Locator params not specified. Specify \\"p\\" search parameter in the URL, which should be JSON serialized object of locator params."` + ); +}); + +test('throws if params are not JSON', () => { + expect(() => parseSearchParams(`?l=LOCATOR&v=1.1.1&p=asdf`)).toThrowErrorMatchingInlineSnapshot( + `"Could not parse locator params. Locator params must be serialized as JSON and set at \\"p\\" URL search parameter."` + ); +}); diff --git a/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts new file mode 100644 index 0000000000000..a60c1d1b68a97 --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import { i18n } from '@kbn/i18n'; +import type { RedirectOptions } from '../redirect_manager'; + +/** + * Parses redirect endpoint URL path search parameters. Expects them in the + * following form: + * + * ``` + * /r?l=&v=&p= + * ``` + * + * @param urlSearch Search part of URL path. + * @returns Parsed out locator ID, version, and locator params. + */ +export function parseSearchParams(urlSearch: string): RedirectOptions { + const search = new URLSearchParams(urlSearch); + const id = search.get('l'); + const version = search.get('v'); + const paramsJson = search.get('p'); + + if (!id) { + const message = i18n.translate( + 'share.urlService.redirect.RedirectManager.missingParamLocator', + { + defaultMessage: + 'Locator ID not specified. Specify "l" search parameter in the URL, which should be an existing locator ID.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because of missing locator ID.', + } + ); + throw new Error(message); + } + + if (!version) { + const message = i18n.translate( + 'share.urlService.redirect.RedirectManager.missingParamVersion', + { + defaultMessage: + 'Locator params version not specified. Specify "v" search parameter in the URL, which should be the release version of Kibana when locator params were generated.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because of missing version parameter.', + } + ); + throw new Error(message); + } + + if (!paramsJson) { + const message = i18n.translate('share.urlService.redirect.RedirectManager.missingParamParams', { + defaultMessage: + 'Locator params not specified. Specify "p" search parameter in the URL, which should be JSON serialized object of locator params.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because of missing params parameter.', + }); + throw new Error(message); + } + + let params: unknown & SerializableState; + try { + params = JSON.parse(paramsJson); + } catch { + const message = i18n.translate('share.urlService.redirect.RedirectManager.invalidParamParams', { + defaultMessage: + 'Could not parse locator params. Locator params must be serialized as JSON and set at "p" URL search parameter.', + description: + 'Error displayed to user in redirect endpoint when redirection cannot be performed successfully, because locator parameters could not be parsed as JSON.', + }); + throw new Error(message); + } + + return { + id, + version, + params, + }; +} diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index d11e1cf78c960..13caa3c33fa82 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1743,6 +1743,137 @@ } } }, + "r": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "apm": { "properties": { "appId": { From 9773e3f6780095e02240cc4770f4b9d58e12066a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 6 Jul 2021 12:07:54 +0100 Subject: [PATCH 06/14] skip failing es promotion suite (#104413) --- test/functional/apps/context/_discover_navigation.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index a09be8b35ba8f..6a2298ba48cb4 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -32,7 +32,8 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); - describe('context link in discover', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104413 + describe.skip('context link in discover', () => { before(async () => { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ From f6fc6c1a3d552764c25fa4e3ac28cc99eea32c17 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 6 Jul 2021 14:36:24 +0200 Subject: [PATCH 07/14] Fix new terms enum API when field meta is not passed for autocomplete value suggestions (#104141) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/autocomplete/terms_agg.test.ts | 48 +++++++++++++++++++ .../server/autocomplete/terms_enum.test.ts | 43 +++++++++++++++++ .../data/server/autocomplete/terms_enum.ts | 2 +- 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/server/autocomplete/terms_agg.test.ts b/src/plugins/data/server/autocomplete/terms_agg.test.ts index e4652c2c422e2..ae991e289a715 100644 --- a/src/plugins/data/server/autocomplete/terms_agg.test.ts +++ b/src/plugins/data/server/autocomplete/terms_agg.test.ts @@ -32,6 +32,8 @@ const mockResponse = { }, } as ApiResponse>; +jest.mock('../index_patterns'); + describe('terms agg suggestions', () => { beforeEach(() => { const requestHandlerContext = coreMock.createRequestHandlerContext(); @@ -86,4 +88,50 @@ describe('terms agg suggestions', () => { ] `); }); + + it('calls the _search API with a terms agg and fallback to fieldName when field is null', async () => { + const result = await termsAggSuggestions( + configMock, + savedObjectsClientMock, + esClientMock, + 'index', + 'fieldName', + 'query', + [] + ); + + const [[args]] = esClientMock.search.mock.calls; + + expect(args).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "suggestions": Object { + "terms": Object { + "execution_hint": "map", + "field": "fieldName", + "include": "query.*", + "shard_size": 10, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [], + }, + }, + "size": 0, + "terminate_after": 98430, + "timeout": "4513ms", + }, + "index": "index", + } + `); + expect(result).toMatchInlineSnapshot(` + Array [ + "whoa", + "amazing", + ] + `); + }); }); diff --git a/src/plugins/data/server/autocomplete/terms_enum.test.ts b/src/plugins/data/server/autocomplete/terms_enum.test.ts index be8f179db29c0..41eaf3f4032ab 100644 --- a/src/plugins/data/server/autocomplete/terms_enum.test.ts +++ b/src/plugins/data/server/autocomplete/terms_enum.test.ts @@ -22,6 +22,8 @@ const mockResponse = { body: { terms: ['whoa', 'amazing'] }, }; +jest.mock('../index_patterns'); + describe('_terms_enum suggestions', () => { beforeEach(() => { const requestHandlerContext = coreMock.createRequestHandlerContext(); @@ -71,4 +73,45 @@ describe('_terms_enum suggestions', () => { `); expect(result).toEqual(mockResponse.body.terms); }); + + it('calls the _terms_enum API and fallback to fieldName when field is null', async () => { + const result = await termsEnumSuggestions( + configMock, + savedObjectsClientMock, + esClientMock, + 'index', + 'fieldName', + 'query', + [] + ); + + const [[args]] = esClientMock.transport.request.mock.calls; + + expect(args).toMatchInlineSnapshot(` + Object { + "body": Object { + "field": "fieldName", + "index_filter": Object { + "bool": Object { + "must": Array [ + Object { + "terms": Object { + "_tier": Array [ + "data_hot", + "data_warm", + "data_content", + ], + }, + }, + ], + }, + }, + "string": "query", + }, + "method": "POST", + "path": "/index/_terms_enum", + } + `); + expect(result).toEqual(mockResponse.body.terms); + }); }); diff --git a/src/plugins/data/server/autocomplete/terms_enum.ts b/src/plugins/data/server/autocomplete/terms_enum.ts index c2452b0a099d0..40329586a3621 100644 --- a/src/plugins/data/server/autocomplete/terms_enum.ts +++ b/src/plugins/data/server/autocomplete/terms_enum.ts @@ -36,7 +36,7 @@ export async function termsEnumSuggestions( method: 'POST', path: encodeURI(`/${index}/_terms_enum`), body: { - field: field?.name ?? field, + field: field?.name ?? fieldName, string: query, index_filter: { bool: { From 2d48f7fb1143b3575dc25586d3f4838792517f58 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 6 Jul 2021 14:41:24 +0200 Subject: [PATCH 08/14] Add `userSetup` plugin skeleton. (#101610) --- .github/CODEOWNERS | 1 + docs/developer/plugin-list.asciidoc | 4 ++++ packages/kbn-optimizer/limits.yml | 1 + src/plugins/user_setup/README.md | 3 +++ src/plugins/user_setup/jest.config.js | 13 +++++++++++ src/plugins/user_setup/kibana.json | 13 +++++++++++ src/plugins/user_setup/public/app.tsx | 27 ++++++++++++++++++++++ src/plugins/user_setup/public/index.ts | 11 +++++++++ src/plugins/user_setup/public/plugin.tsx | 29 ++++++++++++++++++++++++ src/plugins/user_setup/server/config.ts | 16 +++++++++++++ src/plugins/user_setup/server/index.ts | 19 ++++++++++++++++ src/plugins/user_setup/server/plugin.ts | 17 ++++++++++++++ src/plugins/user_setup/tsconfig.json | 12 ++++++++++ 13 files changed, 166 insertions(+) create mode 100644 src/plugins/user_setup/README.md create mode 100644 src/plugins/user_setup/jest.config.js create mode 100644 src/plugins/user_setup/kibana.json create mode 100644 src/plugins/user_setup/public/app.tsx create mode 100644 src/plugins/user_setup/public/index.ts create mode 100644 src/plugins/user_setup/public/plugin.tsx create mode 100644 src/plugins/user_setup/server/config.ts create mode 100644 src/plugins/user_setup/server/index.ts create mode 100644 src/plugins/user_setup/server/plugin.ts create mode 100644 src/plugins/user_setup/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f2d6749813013..5fcb619af6570 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -252,6 +252,7 @@ /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core /src/plugins/security_oss/ @elastic/kibana-security /src/plugins/spaces_oss/ @elastic/kibana-security +/src/plugins/user_setup/ @elastic/kibana-security /test/security_functional/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b4be27eee5ed2..ffc918af92514 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -256,6 +256,10 @@ In general this plugin provides: |The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. At the same time, it provides necessary the APIs for other services (i.e.: telemetry, monitoring, ...) to consume that usage data. +|{kib-repo}blob/{branch}/src/plugins/user_setup/README.md[userSetup] +|The plugin provides UI and APIs for the interactive setup mode. + + |{kib-repo}blob/{branch}/src/plugins/vis_default_editor/README.md[visDefaultEditor] |The default editor is used in most primary visualizations, e.x. Area, Data table, Pie, etc. It acts as a container for a particular visualization and options tabs. Contains the default "Data" tab in public/components/sidebar/data_tab.tsx. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 6627b644daec7..2c7f194d7da98 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -112,3 +112,4 @@ pageLoadAssetSize: visTypePie: 35583 expressionRevealImage: 25675 cases: 144442 + userSetup: 18532 diff --git a/src/plugins/user_setup/README.md b/src/plugins/user_setup/README.md new file mode 100644 index 0000000000000..61ec964f5bb80 --- /dev/null +++ b/src/plugins/user_setup/README.md @@ -0,0 +1,3 @@ +# `userSetup` plugin + +The plugin provides UI and APIs for the interactive setup mode. diff --git a/src/plugins/user_setup/jest.config.js b/src/plugins/user_setup/jest.config.js new file mode 100644 index 0000000000000..75e355e230c5d --- /dev/null +++ b/src/plugins/user_setup/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/user_setup'], +}; diff --git a/src/plugins/user_setup/kibana.json b/src/plugins/user_setup/kibana.json new file mode 100644 index 0000000000000..192fd42cd3e26 --- /dev/null +++ b/src/plugins/user_setup/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "userSetup", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin provides UI and APIs for the interactive setup mode.", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["userSetup"], + "server": true, + "ui": true +} diff --git a/src/plugins/user_setup/public/app.tsx b/src/plugins/user_setup/public/app.tsx new file mode 100644 index 0000000000000..2b6b708953972 --- /dev/null +++ b/src/plugins/user_setup/public/app.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiPageTemplate, EuiPanel, EuiText } from '@elastic/eui'; +import React from 'react'; + +export const App = () => { + return ( + + + Kibana server is not ready yet. + + + ); +}; diff --git a/src/plugins/user_setup/public/index.ts b/src/plugins/user_setup/public/index.ts new file mode 100644 index 0000000000000..153bc92a0dd08 --- /dev/null +++ b/src/plugins/user_setup/public/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. + */ + +import { UserSetupPlugin } from './plugin'; + +export const plugin = () => new UserSetupPlugin(); diff --git a/src/plugins/user_setup/public/plugin.tsx b/src/plugins/user_setup/public/plugin.tsx new file mode 100644 index 0000000000000..677c27cc456dc --- /dev/null +++ b/src/plugins/user_setup/public/plugin.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import type { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { App } from './app'; + +export class UserSetupPlugin implements Plugin { + public setup(core: CoreSetup) { + core.application.register({ + id: 'userSetup', + title: 'User Setup', + chromeless: true, + mount: (params) => { + ReactDOM.render(, params.element); + return () => ReactDOM.unmountComponentAtNode(params.element); + }, + }); + } + + public start(core: CoreStart) {} +} diff --git a/src/plugins/user_setup/server/config.ts b/src/plugins/user_setup/server/config.ts new file mode 100644 index 0000000000000..b16c51bcbda09 --- /dev/null +++ b/src/plugins/user_setup/server/config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; + +export type ConfigType = TypeOf; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); diff --git a/src/plugins/user_setup/server/index.ts b/src/plugins/user_setup/server/index.ts new file mode 100644 index 0000000000000..2a43cbbf65c9d --- /dev/null +++ b/src/plugins/user_setup/server/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from 'src/core/server'; + +import { ConfigSchema } from './config'; +import { UserSetupPlugin } from './plugin'; + +export const config: PluginConfigDescriptor> = { + schema: ConfigSchema, +}; + +export const plugin = () => new UserSetupPlugin(); diff --git a/src/plugins/user_setup/server/plugin.ts b/src/plugins/user_setup/server/plugin.ts new file mode 100644 index 0000000000000..918c9a2007935 --- /dev/null +++ b/src/plugins/user_setup/server/plugin.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CoreSetup, CoreStart, Plugin } from 'src/core/server'; + +export class UserSetupPlugin implements Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/src/plugins/user_setup/tsconfig.json b/src/plugins/user_setup/tsconfig.json new file mode 100644 index 0000000000000..d211a70f12df3 --- /dev/null +++ b/src/plugins/user_setup/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*"], + "references": [{ "path": "../../core/tsconfig.json" }] +} From 75187b6aeef8a9c2aaab258de3f60f38cd9bb878 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 6 Jul 2021 09:16:34 -0400 Subject: [PATCH 09/14] [Fleet] Fix powershell command to add fleet server (#104342) --- .../components/install_command_utils.test.ts | 26 +++++++++---------- .../components/install_command_utils.ts | 17 ++++++------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts index e9e7e09207992..b4e7982c52f7b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts @@ -31,8 +31,8 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ + ".\\\\elastic-agent.exe install -f \` + --fleet-server-es=http://elasticsearch:9200 \` --fleet-server-service-token=service-token-1" `); }); @@ -78,9 +78,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + ".\\\\elastic-agent.exe install -f \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` --fleet-server-policy=policy-1" `); }); @@ -137,14 +137,14 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install --url=http://fleetserver:8220 \\\\ - -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ - --fleet-server-policy=policy-1 \\\\ - --certificate-authorities= \\\\ - --fleet-server-es-ca= \\\\ - --fleet-server-cert= \\\\ + ".\\\\elastic-agent.exe install --url=http://fleetserver:8220 \` + -f \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` + --fleet-server-policy=policy-1 \` + --certificate-authorities= \` + --fleet-server-es-ca= \` + --fleet-server-cert= \` --fleet-server-cert-key=" `); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts index b91c4b60aa713..e129d7a4d5b4e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts @@ -16,22 +16,23 @@ export function getInstallCommandForPlatform( isProductionDeployment?: boolean ) { let commandArguments = ''; + const newLineSeparator = platform === 'windows' ? '`' : '\\'; if (isProductionDeployment && fleetServerHost) { - commandArguments += `--url=${fleetServerHost} \\\n`; + commandArguments += `--url=${fleetServerHost} ${newLineSeparator}\n`; } - commandArguments += ` -f \\\n --fleet-server-es=${esHost}`; - commandArguments += ` \\\n --fleet-server-service-token=${serviceToken}`; + commandArguments += ` -f ${newLineSeparator}\n --fleet-server-es=${esHost}`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-service-token=${serviceToken}`; if (policyId) { - commandArguments += ` \\\n --fleet-server-policy=${policyId}`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-policy=${policyId}`; } if (isProductionDeployment) { - commandArguments += ` \\\n --certificate-authorities=`; - commandArguments += ` \\\n --fleet-server-es-ca=`; - commandArguments += ` \\\n --fleet-server-cert=`; - commandArguments += ` \\\n --fleet-server-cert-key=`; + commandArguments += ` ${newLineSeparator}\n --certificate-authorities=`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-es-ca=`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-cert=`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-cert-key=`; } switch (platform) { From 1ae7afd1ca45e0d1a2ebdb3cd5f419970b3b7477 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 6 Jul 2021 15:25:14 +0200 Subject: [PATCH 10/14] Fix Kibana page crash on redirect navigation when timeline is open (#104288) --- .../public/common/components/endpoint/route_capture.tsx | 6 ------ .../public/common/components/url_state/index.test.tsx | 8 ++++++++ .../common/components/url_state/index_mocked.test.tsx | 8 ++++++++ .../public/common/components/url_state/use_url_state.tsx | 7 ++++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx index a5e0c90402df4..ebd25eef87cb7 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx @@ -9,8 +9,6 @@ import React, { memo, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { AppLocation } from '../../../../common/endpoint/types'; -import { timelineActions } from '../../../timelines/store/timeline'; -import { TimelineId } from '../../../../../timelines/common'; /** * This component should be used above all routes, but below the Provider. @@ -20,10 +18,6 @@ export const RouteCapture = memo(({ children }) => { const location: AppLocation = useLocation(); const dispatch = useDispatch(); - useEffect(() => { - dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); - }, [dispatch, location.pathname]); - useEffect(() => { dispatch({ type: 'userChangedUrl', payload: location }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index b40799895e8a2..18b99adca3a55 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -59,6 +59,14 @@ jest.mock('../../lib/kibana', () => ({ }, })); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + }; +}); + describe('UrlStateContainer', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index e178aba188d11..3175656f12071 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -31,6 +31,14 @@ jest.mock('../../lib/kibana', () => ({ }), })); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + }; +}); + describe('UrlStateContainer - lodash.throttle mocked to test update url', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index 487463dfd9d7d..87e17ba7691cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -9,6 +9,7 @@ import { difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; import { useKibana } from '../../lib/kibana'; import { CONSTANTS, UrlStateType } from './constants'; import { @@ -31,6 +32,8 @@ import { UrlState, } from './types'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { TimelineId } from '../../../../../timelines/common'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -71,6 +74,7 @@ export const useUrlStateHooks = ({ const [isInitializing, setIsInitializing] = useState(true); const { filterManager, savedQueries } = useKibana().services.data.query; const prevProps = usePrevious({ pathName, pageName, urlState }); + const dispatch = useDispatch(); const handleInitialize = (type: UrlStateType, needUpdate?: boolean) => { let mySearch = search; @@ -222,9 +226,10 @@ export const useUrlStateHooks = ({ }); } else if (pathName !== prevProps.pathName) { handleInitialize(type, isDetectionsPages(pageName)); + dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInitializing, history, pathName, pageName, prevProps, urlState]); + }, [isInitializing, history, pathName, pageName, prevProps, urlState, dispatch]); useEffect(() => { document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; From d20de1222c92986bafeafa4386162981602695fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Tue, 6 Jul 2021 15:33:41 +0200 Subject: [PATCH 11/14] [Fleet] Tweak agent permissions (#104415) --- x-pack/plugins/fleet/server/services/agent_policy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index cff70737be6ee..8302983316316 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -773,10 +773,10 @@ class AgentPolicyService { ) { const names: string[] = []; if (fullAgentPolicy.agent.monitoring.logs) { - names.push(`logs-elastic_agent.*-${monitoringNamespace}`); + names.push(`logs-elastic_agent*-${monitoringNamespace}`); } if (fullAgentPolicy.agent.monitoring.metrics) { - names.push(`metrics-elastic_agent.*-${monitoringNamespace}`); + names.push(`metrics-elastic_agent*-${monitoringNamespace}`); } permissions._elastic_agent_checks.indices = [ From df8f870f38a47cda9acdb6d82a1b6c0a08219da1 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 6 Jul 2021 09:49:50 -0400 Subject: [PATCH 12/14] [Lens] i18n tinymath help text (#104205) * [Lens] i18n tinymath help text * Fix i18n ids for remaining strings Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../operations/definitions/formula/util.ts | 126 +++++++++++------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts index 9806cdaad637e..445df21a6067e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -106,18 +106,20 @@ export const tinymathFunctions: Record< type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.addFunction.markdown', { + defaultMessage: ` Adds up two numbers. Also works with + symbol Example: Calculate the sum of two fields -${'`sum(price) + sum(tax)`'} +\`sum(price) + sum(tax)\` Example: Offset count by a static value -${'`add(count(), 5)`'} +\`add(count(), 5)\` `, + }), }, subtract: { positionalArguments: [ @@ -130,13 +132,15 @@ ${'`add(count(), 5)`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.subtractFunction.markdown', { + defaultMessage: ` Subtracts the first number from the second number. -Also works with ${'`-`'} symbol +Also works with \`-\` symbol Example: Calculate the range of a field -${'`subtract(max(bytes), min(bytes))`'} +\`subtract(max(bytes), min(bytes))\` `, + }), }, multiply: { positionalArguments: [ @@ -149,16 +153,18 @@ ${'`subtract(max(bytes), min(bytes))`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.multiplyFunction.markdown', { + defaultMessage: ` Multiplies two numbers. -Also works with ${'`*`'} symbol. +Also works with \`*\` symbol. Example: Calculate price after current tax rate -${'`sum(bytes) * last_value(tax_rate)`'} +\`sum(bytes) * last_value(tax_rate)\` Example: Calculate price after constant tax rate -${'`multiply(sum(price), 1.2)`'} +\`multiply(sum(price), 1.2)\` `, + }), }, divide: { positionalArguments: [ @@ -171,15 +177,17 @@ ${'`multiply(sum(price), 1.2)`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.divideFunction.markdown', { + defaultMessage: ` Divides the first number by the second number. -Also works with ${'`/`'} symbol +Also works with \`/\` symbol Example: Calculate profit margin -${'`sum(profit) / sum(revenue)`'} +\`sum(profit) / sum(revenue)\` -Example: ${'`divide(sum(bytes), 2)`'} +Example: \`divide(sum(bytes), 2)\` `, + }), }, abs: { positionalArguments: [ @@ -188,11 +196,13 @@ Example: ${'`divide(sum(bytes), 2)`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.absFunction.markdown', { + defaultMessage: ` Calculates absolute value. A negative value is multiplied by -1, a positive value stays the same. -Example: Calculate average distance to sea level ${'`abs(average(altitude))`'} +Example: Calculate average distance to sea level \`abs(average(altitude))\` `, + }), }, cbrt: { positionalArguments: [ @@ -201,12 +211,14 @@ Example: Calculate average distance to sea level ${'`abs(average(altitude))`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.cbrtFunction.markdown', { + defaultMessage: ` Cube root of value. Example: Calculate side length from volume -${'`cbrt(last_value(volume))`'} +\`cbrt(last_value(volume))\` `, + }), }, ceil: { positionalArguments: [ @@ -215,13 +227,14 @@ ${'`cbrt(last_value(volume))`'} type: getTypeI18n('number'), }, ], - // signature: 'ceil(value: number)', - help: ` + help: i18n.translate('xpack.lens.formula.ceilFunction.markdown', { + defaultMessage: ` Ceiling of value, rounds up. Example: Round up price to the next dollar -${'`ceil(sum(price))`'} +\`ceil(sum(price))\` `, + }), }, clamp: { positionalArguments: [ @@ -238,8 +251,8 @@ ${'`ceil(sum(price))`'} type: getTypeI18n('number'), }, ], - // signature: 'clamp(value: number, minimum: number, maximum: number)', - help: ` + help: i18n.translate('xpack.lens.formula.clampFunction.markdown', { + defaultMessage: ` Limits the value from a minimum to maximum. Example: Make sure to catch outliers @@ -251,6 +264,7 @@ clamp( ) \`\`\` `, + }), }, cube: { positionalArguments: [ @@ -259,12 +273,14 @@ clamp( type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.cubeFunction.markdown', { + defaultMessage: ` Calculates the cube of a number. Example: Calculate volume from side length -${'`cube(last_value(length))`'} +\`cube(last_value(length))\` `, + }), }, exp: { positionalArguments: [ @@ -273,13 +289,15 @@ ${'`cube(last_value(length))`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.expFunction.markdown', { + defaultMessage: ` Raises *e* to the nth power. Example: Calculate the natural exponential function -${'`exp(last_value(duration))`'} +\`exp(last_value(duration))\` `, + }), }, fix: { positionalArguments: [ @@ -288,12 +306,14 @@ ${'`exp(last_value(duration))`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.fixFunction.markdown', { + defaultMessage: ` For positive values, takes the floor. For negative values, takes the ceiling. Example: Rounding towards zero -${'`fix(sum(profit))`'} +\`fix(sum(profit))\` `, + }), }, floor: { positionalArguments: [ @@ -302,12 +322,14 @@ ${'`fix(sum(profit))`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.floorFunction.markdown', { + defaultMessage: ` Round down to nearest integer value Example: Round down a price -${'`floor(sum(price))`'} +\`floor(sum(price))\` `, + }), }, log: { positionalArguments: [ @@ -322,7 +344,8 @@ ${'`floor(sum(price))`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.logFunction.markdown', { + defaultMessage: ` Logarithm with optional base. The natural base *e* is used as default. Example: Calculate number of bits required to store values @@ -331,17 +354,8 @@ log(sum(bytes)) log(sum(bytes), 2) \`\`\` `, + }), }, - // TODO: check if this is valid for Tinymath - // log10: { - // positionalArguments: [ - // { name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }), type: getTypeI18n('number') }, - // ], - // help: ` - // Base 10 logarithm. - // Example: ${'`log10(sum(bytes))`'} - // `, - // }, mod: { positionalArguments: [ { @@ -353,12 +367,14 @@ log(sum(bytes), 2) type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.modFunction.markdown', { + defaultMessage: ` Remainder after dividing the function by a number Example: Calculate last three digits of a value -${'`mod(sum(price), 1000)`'} +\`mod(sum(price), 1000)\` `, + }), }, pow: { positionalArguments: [ @@ -371,12 +387,14 @@ ${'`mod(sum(price), 1000)`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.powFunction.markdown', { + defaultMessage: ` Raises the value to a certain power. The second argument is required Example: Calculate volume based on side length -${'`pow(last_value(length), 3)`'} +\`pow(last_value(length), 3)\` `, + }), }, round: { positionalArguments: [ @@ -391,7 +409,8 @@ ${'`pow(last_value(length), 3)`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.roundFunction.markdown', { + defaultMessage: ` Rounds to a specific number of decimal places, default of 0 Examples: Round to the cent @@ -400,6 +419,7 @@ round(sum(bytes)) round(sum(bytes), 2) \`\`\` `, + }), }, sqrt: { positionalArguments: [ @@ -408,12 +428,14 @@ round(sum(bytes), 2) type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.sqrtFunction.markdown', { + defaultMessage: ` Square root of a positive value only Example: Calculate side length based on area -${'`sqrt(last_value(area))`'} +\`sqrt(last_value(area))\` `, + }), }, square: { positionalArguments: [ @@ -422,12 +444,14 @@ ${'`sqrt(last_value(area))`'} type: getTypeI18n('number'), }, ], - help: ` + help: i18n.translate('xpack.lens.formula.squareFunction.markdown', { + defaultMessage: ` Raise the value to the 2nd power Example: Calculate area based on side length -${'`square(last_value(length))`'} +\`square(last_value(length))\` `, + }), }, }; From 57fdadbbec2cdd4681d1210a788405ac2a947a21 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 6 Jul 2021 10:02:47 -0400 Subject: [PATCH 13/14] [Docs] Add auth_provider_hint to authentication docs (#104132) --- docs/user/security/authentication/index.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index faa980fe833cb..5506e7ab375a2 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -65,6 +65,10 @@ image::user/security/images/kibana-login.png["Login Selector UI"] For more information, refer to <>. +TIP: If you have multiple authentication providers configured, you can use the `auth_provider_hint` URL query parameter to create a deep +link to any provider and bypass the Login Selector UI. Using the `kibana.yml` above as an example, you can add `?auth_provider_hint=basic1` +to the login page URL, which will take you directly to the basic login page. + [[basic-authentication]] ==== Basic authentication From 434568abe40c192ebfc23c9f4b3c77edcc232dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 6 Jul 2021 10:04:31 -0400 Subject: [PATCH 14/14] [APM] Blank page when navigating to errors metadata (#104322) * using history.location instead of location * removing consoles --- .../components/app/error_group_details/detail_view/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx index be3895967d4dc..5a56b64374537 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx @@ -157,9 +157,9 @@ export function DetailView({ errorGroup, urlParams }: Props) { { history.replace({ - ...location, + ...history.location, search: fromQuery({ - ...toQuery(location.search), + ...toQuery(history.location.search), detailTab: key, }), });