;
// (undocumented)
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 65857f02c883d9..54a3fe9e4399c0 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 809cb15c3e9606..18f59186f61831 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 00000000000000..2ae376e787d2f7
--- /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 00000000000000..c16392164e3e4a
--- /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 00000000000000..f7168b46e7fca6
--- /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 00000000000000..6768c1aff810a3
--- /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 680fb2231fc48d..bae57b6d8a31d2 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 00000000000000..be86cfe4017133
--- /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 5ee3156534c5ef..1f999b59ddb617 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 00000000000000..eb9c6d0d109063
--- /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 893108b56bcfad..adc28556d7a3cc 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 00000000000000..8fa88e9c570bd4
--- /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 00000000000000..cd31f2b80099be
--- /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 00000000000000..716848427c638a
--- /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}