();
- for (const baseMigrationVersion of Object.keys(baseEmbeddableMigrations)) {
- uniqueVersions.add(baseMigrationVersion);
- }
- const factories = this.embeddableFactories.values();
- for (const factory of factories) {
- Object.keys(factory.migrations).forEach((version) => uniqueVersions.add(version));
- }
- const enhancements = this.enhancements.values();
- for (const enhancement of enhancements) {
- Object.keys(enhancement.migrations).forEach((version) => uniqueVersions.add(version));
- }
- return Array.from(uniqueVersions);
- };
}
diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md
index d5c7ce29bab9..f8f3dcb0aa0b 100644
--- a/src/plugins/embeddable/server/server.api.md
+++ b/src/plugins/embeddable/server/server.api.md
@@ -23,8 +23,10 @@ export interface EmbeddableRegistryDefinition {
+ // Warning: (ae-forgotten-export) The symbol "MigrateFunctionsObject" needs to be exported by the entry point index.d.ts
+ //
// (undocumented)
- getMigrationVersions: () => string[];
+ getAllMigrations: () => MigrateFunctionsObject;
// (undocumented)
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
// (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 65857f02c883..54a3fe9e4399 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 809cb15c3e96..1f417002f276 100644
--- a/src/plugins/kibana_utils/common/persistable_state/index.ts
+++ b/src/plugins/kibana_utils/common/persistable_state/index.ts
@@ -6,87 +6,6 @@
* 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';
+export { mergeMigrationFunctionMaps } from './merge_migration_function_map';
diff --git a/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.test.ts b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.test.ts
new file mode 100644
index 000000000000..9a6d774d7047
--- /dev/null
+++ b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.test.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { mergeMigrationFunctionMaps } from './merge_migration_function_map';
+
+describe('mergeSavedObjectMigrationMaps', () => {
+ const obj1 = {
+ '7.12.1': (state: number) => state + 1,
+ '7.12.2': (state: number) => state + 2,
+ };
+
+ const obj2 = {
+ '7.12.0': (state: number) => state - 2,
+ '7.12.2': (state: number) => state + 2,
+ };
+
+ test('correctly merges two saved object migration maps', () => {
+ const result = mergeMigrationFunctionMaps(obj1, obj2);
+ expect(result['7.12.0'](5)).toEqual(3);
+ expect(result['7.12.1'](5)).toEqual(6);
+ expect(result['7.12.2'](5)).toEqual(9);
+ });
+});
diff --git a/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts
new file mode 100644
index 000000000000..fc48ab119b02
--- /dev/null
+++ b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 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 { mergeWith } from 'lodash';
+import { MigrateFunctionsObject, MigrateFunction, SerializableState } from './types';
+
+export const mergeMigrationFunctionMaps = (
+ obj1: MigrateFunctionsObject,
+ obj2: MigrateFunctionsObject
+) => {
+ const customizer = (objValue: MigrateFunction, srcValue: MigrateFunction) => {
+ if (!srcValue || !objValue) {
+ return srcValue || objValue;
+ }
+ return (state: SerializableState) => objValue(srcValue(state));
+ };
+
+ return mergeWith({ ...obj1 }, obj2, customizer);
+};
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 000000000000..2ae376e787d2
--- /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 000000000000..c16392164e3e
--- /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 000000000000..ba2b923e3e47
--- /dev/null
+++ b/src/plugins/kibana_utils/common/persistable_state/types.ts
@@ -0,0 +1,178 @@
+/*
+ * 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;
+
+/**
+ * migrate function runs the specified migration
+ * @param state
+ * @param version
+ */
+export type PersistableStateMigrateFn = (
+ state: SerializableState,
+ version: string
+) => SerializableState;
+
+/**
+ * @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[] };
+
+ /**
+ * 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;
+
+ /**
+ * returns all registered migrations
+ */
+ getAllMigrations?: () => MigrateFunctionsObject;
+}
diff --git a/src/plugins/share/common/mocks.ts b/src/plugins/share/common/mocks.ts
new file mode 100644
index 000000000000..6768c1aff810
--- /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 680fb2231fc4..bae57b6d8a31 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 000000000000..be86cfe40171
--- /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 5ee3156534c5..1f999b59ddb6 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 000000000000..eb9c6d0d1090
--- /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 893108b56bcf..adc28556d7a3 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 000000000000..8fa88e9c570b
--- /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 000000000000..cd31f2b80099
--- /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 000000000000..716848427c63
--- /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 000000000000..805213b73fdd
--- /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 000000000000..a70ae5eb096a
--- /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 000000000000..8dbc5f4e0ab1
--- /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 000000000000..f610268f529b
--- /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 000000000000..6148249f5a04
--- /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 000000000000..2b9c3a50758e
--- /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 000000000000..f8d8d6a6295d
--- /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 000000000000..12c6424182a8
--- /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 000000000000..418e21cfd405
--- /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 000000000000..a60c1d1b68a9
--- /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 d11e1cf78c96..13caa3c33fa8 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": {
diff --git a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
index 1409e4dd2c90..8466093664d0 100644
--- a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
+++ b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts
@@ -18,6 +18,7 @@ import {
contextServiceMock,
loggingSystemMock,
metricsServiceMock,
+ executionContextServiceMock,
} from '../../../../../core/server/mocks';
import { createHttpServer } from '../../../../../core/server/test_utils';
import { registerStatsRoute } from '../stats';
@@ -37,6 +38,7 @@ describe('/api/stats', () => {
server = createHttpServer();
httpSetup = await server.setup({
context: contextServiceMock.createSetupContract(),
+ executionContext: executionContextServiceMock.createInternalSetupContract(),
});
overallStatus$ = new BehaviorSubject({
level: ServiceStatusLevels.available,
diff --git a/src/plugins/user_setup/README.md b/src/plugins/user_setup/README.md
new file mode 100644
index 000000000000..61ec964f5bb8
--- /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 000000000000..75e355e230c5
--- /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 000000000000..192fd42cd3e2
--- /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 000000000000..2b6b70895397
--- /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 000000000000..153bc92a0dd0
--- /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 000000000000..677c27cc456d
--- /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 000000000000..b16c51bcbda0
--- /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 000000000000..2a43cbbf65c9
--- /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 000000000000..918c9a200793
--- /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 000000000000..d211a70f12df
--- /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" }]
+}
diff --git a/src/plugins/vis_type_pie/public/utils/get_color_picker.tsx b/src/plugins/vis_type_pie/public/utils/get_color_picker.tsx
index 436ce81d3ce3..628c2d74dc43 100644
--- a/src/plugins/vis_type_pie/public/utils/get_color_picker.tsx
+++ b/src/plugins/vis_type_pie/public/utils/get_color_picker.tsx
@@ -10,7 +10,7 @@ import React, { useCallback } from 'react';
import Color from 'color';
import { LegendColorPicker, Position } from '@elastic/charts';
import { PopoverAnchorPosition, EuiPopover, EuiOutsideClickDetector } from '@elastic/eui';
-import { DatatableRow } from '../../../expressions/public';
+import type { DatatableRow } from '../../../expressions/public';
import type { PersistedState } from '../../../visualizations/public';
import { ColorPicker } from '../../../charts/public';
import { BucketColumns } from '../types';
diff --git a/src/plugins/vis_type_pie/public/utils/get_layers.test.ts b/src/plugins/vis_type_pie/public/utils/get_layers.test.ts
index e0658eaa295f..d6f80b3eb231 100644
--- a/src/plugins/vis_type_pie/public/utils/get_layers.test.ts
+++ b/src/plugins/vis_type_pie/public/utils/get_layers.test.ts
@@ -7,6 +7,8 @@
*/
import { ShapeTreeNode } from '@elastic/charts';
import { PaletteDefinition, SeriesLayer } from '../../../charts/public';
+import { dataPluginMock } from '../../../data/public/mocks';
+import type { DataPublicPluginStart } from '../../../data/public';
import { computeColor } from './get_layers';
import { createMockVisData, createMockBucketColumns, createMockPieParams } from '../mocks';
@@ -14,6 +16,20 @@ const visData = createMockVisData();
const buckets = createMockBucketColumns();
const visParams = createMockPieParams();
const colors = ['color1', 'color2', 'color3', 'color4'];
+const dataMock = dataPluginMock.createStartContract();
+interface RangeProps {
+ gte: number;
+ lt: number;
+}
+
+dataMock.fieldFormats = ({
+ deserialize: jest.fn(() => ({
+ convert: jest.fn((s: RangeProps) => {
+ return `≥ ${s.gte} and < ${s.lt}`;
+ }),
+ })),
+} as unknown) as DataPublicPluginStart['fieldFormats'];
+
export const getPaletteRegistry = () => {
const mockPalette1: jest.Mocked = {
id: 'default',
@@ -60,7 +76,8 @@ describe('computeColor', () => {
visData.rows,
visParams,
getPaletteRegistry(),
- false
+ false,
+ dataMock.fieldFormats
);
expect(color).toEqual(colors[0]);
});
@@ -84,7 +101,8 @@ describe('computeColor', () => {
visData.rows,
visParams,
getPaletteRegistry(),
- false
+ false,
+ dataMock.fieldFormats
);
expect(color).toEqual('color3');
});
@@ -107,8 +125,60 @@ describe('computeColor', () => {
visData.rows,
visParams,
getPaletteRegistry(),
- false
+ false,
+ dataMock.fieldFormats
);
expect(color).toEqual('#000028');
});
+
+ it('returns the overwriteColor for older visualizations with formatted values', () => {
+ const d = ({
+ dataName: {
+ gte: 1000,
+ lt: 2000,
+ },
+ depth: 1,
+ sortIndex: 0,
+ parent: {
+ children: [
+ [
+ {
+ gte: 1000,
+ lt: 2000,
+ },
+ ],
+ [
+ {
+ gte: 2000,
+ lt: 3000,
+ },
+ ],
+ ],
+ depth: 0,
+ sortIndex: 0,
+ },
+ } as unknown) as ShapeTreeNode;
+ const visParamsNew = {
+ ...visParams,
+ distinctColors: true,
+ };
+ const color = computeColor(
+ d,
+ true,
+ { '≥ 1000 and < 2000': '#3F6833' },
+ buckets,
+ visData.rows,
+ visParamsNew,
+ getPaletteRegistry(),
+ false,
+ dataMock.fieldFormats,
+ {
+ id: 'range',
+ params: {
+ id: 'number',
+ },
+ }
+ );
+ expect(color).toEqual('#3F6833');
+ });
});
diff --git a/src/plugins/vis_type_pie/public/utils/get_layers.ts b/src/plugins/vis_type_pie/public/utils/get_layers.ts
index 5a82871bf368..b995df83c0bb 100644
--- a/src/plugins/vis_type_pie/public/utils/get_layers.ts
+++ b/src/plugins/vis_type_pie/public/utils/get_layers.ts
@@ -15,9 +15,9 @@ import {
} from '@elastic/charts';
import { isEqual } from 'lodash';
import { SeriesLayer, PaletteRegistry, lightenColor } from '../../../charts/public';
-import { DataPublicPluginStart } from '../../../data/public';
-import { DatatableRow } from '../../../expressions/public';
-import { BucketColumns, PieVisParams, SplitDimensionParams } from '../types';
+import type { DataPublicPluginStart } from '../../../data/public';
+import type { DatatableRow } from '../../../expressions/public';
+import type { BucketColumns, PieVisParams, SplitDimensionParams } from '../types';
import { getDistinctSeries } from './get_distinct_series';
const EMPTY_SLICE = Symbol('empty_slice');
@@ -30,14 +30,33 @@ export const computeColor = (
rows: DatatableRow[],
visParams: PieVisParams,
palettes: PaletteRegistry | null,
- syncColors: boolean
+ syncColors: boolean,
+ formatter: DataPublicPluginStart['fieldFormats'],
+ format?: BucketColumns['format']
) => {
const { parentSeries, allSeries } = getDistinctSeries(rows, columns);
+ const dataName = d.dataName;
+
+ let formattedLabel = '';
+ if (format) {
+ formattedLabel = formatter.deserialize(format).convert(dataName) ?? '';
+ }
if (visParams.distinctColors) {
- const dataName = d.dataName;
+ let overwriteColor;
+ // this is for supporting old visualizations (created by vislib plugin)
+ // it seems that there for some aggs, the uiState saved from vislib is
+ // different than the es-charts handle it
+ if (overwriteColors.hasOwnProperty(formattedLabel)) {
+ overwriteColor = overwriteColors[formattedLabel];
+ }
+
if (Object.keys(overwriteColors).includes(dataName.toString())) {
- return overwriteColors[dataName];
+ overwriteColor = overwriteColors[dataName];
+ }
+
+ if (overwriteColor) {
+ return overwriteColor;
}
const index = allSeries.findIndex((name) => isEqual(name, dataName));
@@ -80,6 +99,13 @@ export const computeColor = (
}
let overwriteColor;
+ // this is for supporting old visualizations (created by vislib plugin)
+ // it seems that there for some aggs, the uiState saved from vislib is
+ // different than the es-charts handle it
+ if (overwriteColors.hasOwnProperty(formattedLabel)) {
+ overwriteColor = overwriteColors[formattedLabel];
+ }
+
seriesLayers.forEach((layer) => {
if (Object.keys(overwriteColors).includes(layer.name)) {
overwriteColor = overwriteColors[layer.name];
@@ -141,7 +167,7 @@ export const getLayers = (
if (name1 === '__other__' && name2 !== '__other__') return 1;
if (name2 === '__other__' && name1 !== '__other__') return -1;
// metric sorting
- if (sort !== '_key') {
+ if (sort && sort !== '_key') {
if (params?.order === 'desc') {
return node2.value - node1.value;
} else {
@@ -169,7 +195,9 @@ export const getLayers = (
rows,
visParams,
palettes,
- syncColors
+ syncColors,
+ formatter,
+ col.format
);
return outputColor || 'rgba(0,0,0,0)';
diff --git a/test/api_integration/apis/index_patterns/deprecations/index.ts b/test/api_integration/apis/index_patterns/deprecations/index.ts
new file mode 100644
index 000000000000..a3970fc8004c
--- /dev/null
+++ b/test/api_integration/apis/index_patterns/deprecations/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('scripted_fields_deprecations', () => {
+ loadTestFile(require.resolve('./scripted_fields'));
+ });
+}
diff --git a/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts b/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts
new file mode 100644
index 000000000000..168c2b005d80
--- /dev/null
+++ b/test/api_integration/apis/index_patterns/deprecations/scripted_fields.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 expect from '@kbn/expect';
+import type { DeprecationsGetResponse } from 'src/core/server/types';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+
+ describe('scripted field deprecations', () => {
+ before(async () => {
+ await esArchiver.emptyKibanaIndex();
+ await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index');
+ });
+
+ after(async () => {
+ await esArchiver.unload(
+ 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index'
+ );
+ });
+
+ it('no scripted fields deprecations', async () => {
+ const { body } = await supertest.get('/api/deprecations/');
+
+ const { deprecations } = body as DeprecationsGetResponse;
+ const dataPluginDeprecations = deprecations.filter(({ domainId }) => domainId === 'data');
+
+ expect(dataPluginDeprecations.length).to.be(0);
+ });
+
+ it('scripted field deprecation', async () => {
+ const title = `basic_index`;
+ await supertest.post('/api/index_patterns/index_pattern').send({
+ index_pattern: {
+ title,
+ fields: {
+ foo: {
+ name: 'foo',
+ type: 'string',
+ scripted: true,
+ script: "doc['field_name'].value",
+ },
+ bar: {
+ name: 'bar',
+ type: 'number',
+ scripted: true,
+ script: "doc['field_name'].value",
+ },
+ },
+ },
+ });
+
+ const { body } = await supertest.get('/api/deprecations/');
+ const { deprecations } = body as DeprecationsGetResponse;
+ const dataPluginDeprecations = deprecations.filter(({ domainId }) => domainId === 'data');
+
+ expect(dataPluginDeprecations.length).to.be(1);
+ expect(dataPluginDeprecations[0].message).to.contain(title);
+ });
+ });
+}
diff --git a/test/api_integration/apis/index_patterns/index.js b/test/api_integration/apis/index_patterns/index.js
index 656b4e506fa2..3dbe01206afa 100644
--- a/test/api_integration/apis/index_patterns/index.js
+++ b/test/api_integration/apis/index_patterns/index.js
@@ -17,5 +17,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./default_index_pattern'));
loadTestFile(require.resolve('./runtime_fields_crud'));
loadTestFile(require.resolve('./integration'));
+ loadTestFile(require.resolve('./deprecations'));
});
}
diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js
index a09be8b35ba8..6a2298ba48cb 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({
diff --git a/test/functional/apps/dashboard/view_edit.ts b/test/functional/apps/dashboard/view_edit.ts
index b29b07f9df4e..1ca70112c3d1 100644
--- a/test/functional/apps/dashboard/view_edit.ts
+++ b/test/functional/apps/dashboard/view_edit.ts
@@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardName = 'dashboard with filter';
const filterBar = getService('filterBar');
- describe('dashboard view edit mode', function viewEditModeTests() {
+ // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104467
+ describe.skip('dashboard view edit mode', function viewEditModeTests() {
before(async () => {
await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts
index bb75b4441f88..245b895d75b3 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 () {
diff --git a/test/functional/apps/discover/_doc_table_newline.ts b/test/functional/apps/discover/_doc_table_newline.ts
new file mode 100644
index 000000000000..cdb149641348
--- /dev/null
+++ b/test/functional/apps/discover/_doc_table_newline.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const PageObjects = getPageObjects(['common', 'discover']);
+ const find = getService('find');
+ const log = getService('log');
+ const retry = getService('retry');
+ const security = getService('security');
+
+ describe('discover doc table newline handling', function describeIndexTests() {
+ before(async function () {
+ await security.testUser.setRoles(['kibana_admin', 'kibana_message_with_newline']);
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/message_with_newline');
+ await kibanaServer.uiSettings.replace({
+ defaultIndex: 'newline-test',
+ 'doc_table:legacy': true,
+ });
+ await PageObjects.common.navigateToApp('discover');
+ });
+ after(async () => {
+ await security.testUser.restoreDefaults();
+ esArchiver.unload('test/functional/fixtures/es_archiver/message_with_newline');
+ await kibanaServer.uiSettings.unset('defaultIndex');
+ await kibanaServer.uiSettings.unset('doc_table:legacy');
+ });
+
+ it('should break text on newlines', async function () {
+ await PageObjects.discover.clickFieldListItemToggle('message');
+ const dscTableRows = await find.allByCssSelector('.kbnDocTable__row');
+
+ await retry.waitFor('height of multi-line content > single-line content', async () => {
+ const heightWithoutNewline = await dscTableRows[0].getAttribute('clientHeight');
+ const heightWithNewline = await dscTableRows[1].getAttribute('clientHeight');
+ log.debug(`Without newlines: ${heightWithoutNewline}, With newlines: ${heightWithNewline}`);
+ return Number(heightWithNewline) > Number(heightWithoutNewline);
+ });
+ });
+ });
+}
diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts
index 338d17ba31ff..5ab649568672 100644
--- a/test/functional/apps/discover/_field_data.ts
+++ b/test/functional/apps/discover/_field_data.ts
@@ -33,7 +33,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
await PageObjects.common.navigateToApp('discover');
});
- describe('field data', function () {
+ // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466
+ describe.skip('field data', function () {
it('search php should show the correct hit count', async function () {
const expectedHitCount = '445';
await retry.try(async function () {
diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts
index 869fb625e879..b396f172f696 100644
--- a/test/functional/apps/discover/index.ts
+++ b/test/functional/apps/discover/index.ts
@@ -27,6 +27,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_discover'));
loadTestFile(require.resolve('./_discover_histogram'));
loadTestFile(require.resolve('./_doc_table'));
+ loadTestFile(require.resolve('./_doc_table_newline'));
loadTestFile(require.resolve('./_filter_editor'));
loadTestFile(require.resolve('./_errors'));
loadTestFile(require.resolve('./_field_data'));
diff --git a/test/functional/config.js b/test/functional/config.js
index 670488003e56..c2c856517c58 100644
--- a/test/functional/config.js
+++ b/test/functional/config.js
@@ -247,6 +247,20 @@ export default async function ({ readConfigFile }) {
},
kibana: [],
},
+ kibana_message_with_newline: {
+ elasticsearch: {
+ cluster: [],
+ indices: [
+ {
+ names: ['message_with_newline'],
+ privileges: ['read', 'view_index_metadata'],
+ field_security: { grant: ['*'], except: [] },
+ },
+ ],
+ run_as: [],
+ },
+ kibana: [],
+ },
kibana_timefield: {
elasticsearch: {
diff --git a/test/functional/fixtures/es_archiver/message_with_newline/data.json b/test/functional/fixtures/es_archiver/message_with_newline/data.json
new file mode 100644
index 000000000000..3611f2d3878a
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/message_with_newline/data.json
@@ -0,0 +1,36 @@
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:newline-test",
+ "index": ".kibana",
+ "source": {
+ "index-pattern": {
+ "fields": "[]",
+ "title": "newline-test"
+ },
+ "type": "index-pattern"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "1",
+ "index": "newline-test",
+ "source": {
+ "message" : "no new line"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "2",
+ "index": "newline-test",
+ "source": {
+ "message" : "two\nnew\nlines"
+ }
+ }
+}
diff --git a/test/functional/fixtures/es_archiver/message_with_newline/mappings.json b/test/functional/fixtures/es_archiver/message_with_newline/mappings.json
new file mode 100644
index 000000000000..3e2db145e7e3
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/message_with_newline/mappings.json
@@ -0,0 +1,12 @@
+{
+ "type": "index",
+ "value": {
+ "index": "newline-test",
+ "settings": {
+ "index": {
+ "number_of_replicas": "0",
+ "number_of_shards": "1"
+ }
+ }
+ }
+}
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/kibana.json b/test/plugin_functional/plugins/core_plugin_execution_context/kibana.json
new file mode 100644
index 000000000000..625745202e39
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "corePluginExecutionContext",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "configPath": ["core_plugin_execution_context"],
+ "server": true,
+ "ui": false
+}
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/package.json b/test/plugin_functional/plugins/core_plugin_execution_context/package.json
new file mode 100644
index 000000000000..4b932850cfa0
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "core_plugin_execution_context",
+ "version": "1.0.0",
+ "main": "target/test/plugin_functional/plugins/core_plugin_execution_context",
+ "kibana": {
+ "version": "kibana"
+ },
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "scripts": {
+ "kbn": "node ../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
+ }
+}
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/server/index.ts b/test/plugin_functional/plugins/core_plugin_execution_context/server/index.ts
new file mode 100644
index 000000000000..019e30209675
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/server/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 { CorePluginExecutionContext } from './plugin';
+
+export const plugin = () => new CorePluginExecutionContext();
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_execution_context/server/plugin.ts
new file mode 100644
index 000000000000..48889c6d4a45
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/server/plugin.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 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 { Plugin, CoreSetup } from 'kibana/server';
+
+export class CorePluginExecutionContext implements Plugin {
+ public setup(core: CoreSetup, deps: {}) {
+ const router = core.http.createRouter();
+ router.get(
+ {
+ path: '/execution_context/pass',
+ validate: false,
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping();
+ return response.ok({ body: headers || {} });
+ }
+ );
+ }
+
+ public start() {}
+ public stop() {}
+}
diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json
new file mode 100644
index 000000000000..21662b2b64a1
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true
+ },
+ "include": [
+ "index.ts",
+ "server/**/*.ts",
+ ],
+ "exclude": [],
+ "references": [
+ { "path": "../../../../src/core/tsconfig.json" }
+ ]
+}
diff --git a/test/plugin_functional/test_suites/core_plugins/execution_context.ts b/test/plugin_functional/test_suites/core_plugins/execution_context.ts
new file mode 100644
index 000000000000..21bcddd32bc1
--- /dev/null
+++ b/test/plugin_functional/test_suites/core_plugins/execution_context.ts
@@ -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 expect from '@kbn/expect';
+import { PluginFunctionalProviderContext } from '../../services';
+import '../../../../test/plugin_functional/plugins/core_provider_plugin/types';
+
+export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
+ describe('execution context', function () {
+ describe('passed for a client-side operation', () => {
+ const PageObjects = getPageObjects(['common']);
+ const browser = getService('browser');
+
+ before(async () => {
+ await PageObjects.common.navigateToApp('home');
+ });
+
+ it('passes plugin-specific execution context to Elasticsearch server', async () => {
+ expect(
+ await browser.execute(async () => {
+ const coreStart = window._coreProvider.start.core;
+
+ const context = coreStart.executionContext.create({
+ type: 'execution_context_app',
+ name: 'Execution context app',
+ id: '42',
+ // add a non-ASCII symbols to make sure it doesn't break the context propagation mechanism
+ description: 'какое-то странное описание',
+ });
+
+ const result = await coreStart.http.get('/execution_context/pass', {
+ context,
+ });
+
+ return result['x-opaque-id'];
+ })
+ ).to.contain('kibana:execution_context_app:42');
+ });
+ });
+ });
+}
diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts
index 87a153a24570..79850dd63337 100644
--- a/test/plugin_functional/test_suites/core_plugins/index.ts
+++ b/test/plugin_functional/test_suites/core_plugins/index.ts
@@ -12,6 +12,7 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
describe('core plugins', () => {
loadTestFile(require.resolve('./applications'));
loadTestFile(require.resolve('./elasticsearch_client'));
+ loadTestFile(require.resolve('./execution_context'));
loadTestFile(require.resolve('./server_plugins'));
loadTestFile(require.resolve('./ui_plugins'));
loadTestFile(require.resolve('./ui_settings'));
diff --git a/x-pack/package.json b/x-pack/package.json
index 1af3d569e41a..805d8555bf45 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -24,8 +24,5 @@
},
"engines": {
"yarn": "^1.21.1"
- },
- "devDependencies": {
- "@kbn/test": "link:../packages/kbn-test"
}
}
\ No newline at end of file
diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx
new file mode 100644
index 000000000000..7cac4ba97e72
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { Story } from '@storybook/react';
+import React, { ComponentType } from 'react';
+import { CoreStart } from '../../../../../../../../src/core/public';
+import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
+import { createCallApmApi } from '../../../../services/rest/createCallApmApi';
+import { Schema } from './';
+
+export default {
+ title: 'app/Settings/Schema',
+ component: Schema,
+ decorators: [
+ (StoryComponent: ComponentType) => {
+ const coreMock = ({
+ http: {
+ get: () => {
+ return {};
+ },
+ },
+ } as unknown) as CoreStart;
+
+ createCallApmApi(coreMock);
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export const Example: Story = () => {
+ return ;
+};
diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx
index 1005c09cb11b..7a874ed5b803 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx
@@ -5,26 +5,25 @@
* 2.0.
*/
-import React from 'react';
import {
+ EuiButton,
+ EuiCallOut,
+ EuiCard,
EuiFlexGroup,
EuiFlexItem,
- EuiSpacer,
- EuiTitle,
- EuiText,
- EuiCard,
EuiIcon,
- EuiButton,
- EuiCallOut,
EuiLoadingSpinner,
+ EuiSpacer,
+ EuiText,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';
-import rocketLaunchGraphic from './blog-rocket-720x420.png';
+import React from 'react';
import { APMLink } from '../../../shared/Links/apm/APMLink';
+import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';
import { useFleetCloudAgentPolicyHref } from '../../../shared/Links/kibana';
+import rocketLaunchGraphic from './blog-rocket-720x420.png';
interface Props {
onSwitch: () => void;
@@ -285,18 +284,6 @@ export function SchemaOverviewHeading() {
/>
-
-
-
-
- {i18n.translate('xpack.apm.settings.schema.title', {
- defaultMessage: 'Schema',
- })}
-
-
-
-
-
>
);
}
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 be3895967d4d..5a56b6437453 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,
}),
});
diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx
index 4f9e58239855..afdf4c12f41a 100644
--- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx
+++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx
@@ -118,8 +118,8 @@ export function SearchBar({
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
index 8e82a189d75f..a98bdab53cad 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
-import { contextServiceMock } from 'src/core/server/mocks';
+import {
+ contextServiceMock,
+ executionContextServiceMock,
+} from '../../../../../../../../src/core/server/mocks';
import { createHttpServer } from 'src/core/server/test_utils';
import supertest from 'supertest';
import { createApmEventClient } from '.';
@@ -23,6 +26,7 @@ describe('createApmEventClient', () => {
it('cancels a search when a request is aborted', async () => {
const { server: innerServer, createRouter } = await server.setup({
context: contextServiceMock.createSetupContract(),
+ executionContext: executionContextServiceMock.createInternalSetupContract(),
});
const router = createRouter('/');
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index e617ed0510a8..647330eade1f 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -127,6 +127,10 @@ export class APMPlugin
const getCoreStart = () =>
core.getStartServices().then(([coreStart]) => coreStart);
+ const alertsIndexPattern = ruleDataService.getFullAssetName(
+ 'observability-apm*'
+ );
+
const initializeRuleDataTemplates = once(async () => {
const componentTemplateName = ruleDataService.getFullAssetName(
'apm-mappings'
@@ -164,15 +168,16 @@ export class APMPlugin
await ruleDataService.createOrUpdateIndexTemplate({
name: ruleDataService.getFullAssetName('apm-index-template'),
body: {
- index_patterns: [
- ruleDataService.getFullAssetName('observability-apm*'),
- ],
+ index_patterns: [alertsIndexPattern],
composed_of: [
ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
componentTemplateName,
],
},
});
+ await ruleDataService.updateIndexMappingsMatchingPattern(
+ alertsIndexPattern
+ );
});
// initialize eagerly
diff --git a/x-pack/plugins/canvas/i18n/errors.ts b/x-pack/plugins/canvas/i18n/errors.ts
index a55762dce2d2..8b6697e78ca3 100644
--- a/x-pack/plugins/canvas/i18n/errors.ts
+++ b/x-pack/plugins/canvas/i18n/errors.ts
@@ -17,30 +17,6 @@ export const ErrorStrings = {
},
}),
},
- downloadWorkpad: {
- getDownloadFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.downloadWorkpad.downloadFailureErrorMessage', {
- defaultMessage: "Couldn't download workpad",
- }),
- getDownloadRenderedWorkpadFailureErrorMessage: () =>
- i18n.translate(
- 'xpack.canvas.error.downloadWorkpad.downloadRenderedWorkpadFailureErrorMessage',
- {
- defaultMessage: "Couldn't download rendered workpad",
- }
- ),
- getDownloadRuntimeFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.downloadWorkpad.downloadRuntimeFailureErrorMessage', {
- defaultMessage: "Couldn't download Shareable Runtime",
- }),
- getDownloadZippedRuntimeFailureErrorMessage: () =>
- i18n.translate(
- 'xpack.canvas.error.downloadWorkpad.downloadZippedRuntimeFailureErrorMessage',
- {
- defaultMessage: "Couldn't download ZIP file",
- }
- ),
- },
esPersist: {
getSaveFailureTitle: () =>
i18n.translate('xpack.canvas.error.esPersist.saveFailureTitle', {
diff --git a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.js b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.js
deleted file mode 100644
index a27f133c05f4..000000000000
--- a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { debounce } from 'lodash';
-import { getWindow } from '../../lib/get_window';
-
-export class Fullscreen extends React.Component {
- static propTypes = {
- isFullscreen: PropTypes.bool,
- children: PropTypes.func,
- };
-
- state = {
- width: 0,
- height: 0,
- };
-
- UNSAFE_componentWillMount() {
- this.win = getWindow();
- this.setState({
- width: this.win.innerWidth,
- height: this.win.innerHeight,
- });
- }
-
- componentDidMount() {
- this.win.addEventListener('resize', this.onWindowResize);
- }
-
- componentWillUnmount() {
- this.win.removeEventListener('resize', this.onWindowResize);
- }
-
- getWindowSize = () => ({
- width: this.win.innerWidth,
- height: this.win.innerHeight,
- });
-
- onWindowResize = debounce(() => {
- const { width, height } = this.getWindowSize();
- this.setState({ width, height });
- }, 100);
-
- render() {
- const { isFullscreen, children } = this.props;
- const windowSize = {
- width: this.state.width,
- height: this.state.height,
- };
-
- return children({ isFullscreen, windowSize });
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.ts b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.ts
new file mode 100644
index 000000000000..a578afccb4cc
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FC, useEffect, useState } from 'react';
+import { debounce } from 'lodash';
+import { getWindow } from '../../lib/get_window';
+
+interface Props {
+ isFullscreen?: boolean;
+ children: (props: {
+ isFullscreen: boolean;
+ windowSize: { width: number; height: number };
+ }) => JSX.Element;
+}
+
+export const Fullscreen: FC = ({ isFullscreen = false, children }) => {
+ const [width, setWidth] = useState(getWindow().innerWidth);
+ const [height, setHeight] = useState(getWindow().innerHeight);
+
+ const onWindowResize = debounce(({ target }) => {
+ const { innerWidth, innerHeight } = target as Window;
+ setWidth(innerWidth);
+ setHeight(innerHeight);
+ }, 100);
+
+ useEffect(() => {
+ const window = getWindow();
+ window.addEventListener('resize', onWindowResize);
+
+ return () => window.removeEventListener('resize', onWindowResize);
+ });
+
+ const windowSize = {
+ width,
+ height,
+ };
+
+ return children({ isFullscreen, windowSize });
+};
diff --git a/x-pack/plugins/canvas/public/components/fullscreen/index.tsx b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx
index dbf5c378ffa1..953f27ce0b02 100644
--- a/x-pack/plugins/canvas/public/components/fullscreen/index.tsx
+++ b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx
@@ -6,12 +6,18 @@
*/
import React, { FC, useContext } from 'react';
-// @ts-expect-error
import { Fullscreen as Component } from './fullscreen';
import { WorkpadRoutingContext } from '../../routes/workpad';
-export const Fullscreen: FC = ({ children }) => {
+interface Props {
+ children: (props: {
+ isFullscreen: boolean;
+ windowSize: { width: number; height: number };
+ }) => JSX.Element;
+}
+
+export const Fullscreen: FC = ({ children }) => {
const { isFullscreen } = useContext(WorkpadRoutingContext);
return ;
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/index.ts b/x-pack/plugins/canvas/public/components/home/hooks/index.ts
index c4267a985749..dde9a06e4851 100644
--- a/x-pack/plugins/canvas/public/components/home/hooks/index.ts
+++ b/x-pack/plugins/canvas/public/components/home/hooks/index.ts
@@ -8,7 +8,6 @@
export { useCloneWorkpad } from './use_clone_workpad';
export { useCreateWorkpad } from './use_create_workpad';
export { useDeleteWorkpads } from './use_delete_workpad';
-export { useDownloadWorkpad } from './use_download_workpad';
export { useFindTemplates } from './use_find_templates';
export { useFindWorkpads } from './use_find_workpad';
export { useImportWorkpad } from './use_upload_workpad';
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
index 7934a469bb7a..5c01ce631f5a 100644
--- a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
@@ -12,10 +12,12 @@ import { i18n } from '@kbn/i18n';
import { CANVAS, JSON as JSONString } from '../../../../i18n/constants';
import { useNotifyService } from '../../../services';
import { getId } from '../../../lib/get_id';
+import { useCreateWorkpad } from './use_create_workpad';
import type { CanvasWorkpad } from '../../../../types';
export const useImportWorkpad = () => {
const notifyService = useNotifyService();
+ const createWorkpad = useCreateWorkpad();
return useCallback(
(file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => {
@@ -37,7 +39,7 @@ export const useImportWorkpad = () => {
const reader = new FileReader();
// handle reading the uploaded file
- reader.onload = () => {
+ reader.onload = async () => {
try {
const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below.
workpad.id = getId('workpad');
@@ -48,6 +50,7 @@ export const useImportWorkpad = () => {
throw new Error(errors.getMissingPropertiesErrorMessage());
}
+ await createWorkpad(workpad);
onComplete(workpad);
} catch (e) {
notifyService.error(e, {
@@ -62,7 +65,7 @@ export const useImportWorkpad = () => {
// read the uploaded file
reader.readAsText(file);
},
- [notifyService]
+ [notifyService, createWorkpad]
);
};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx
index 8ee0ae108392..962f61651539 100644
--- a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx
@@ -11,8 +11,7 @@ import Dropzone from 'react-dropzone';
import { useNotifyService } from '../../../services';
import { ErrorStrings } from '../../../../i18n';
-import { useImportWorkpad, useCreateWorkpad } from '../hooks';
-import { CanvasWorkpad } from '../../../../types';
+import { useImportWorkpad } from '../hooks';
import { UploadDropzone as Component } from './upload_dropzone.component';
@@ -21,18 +20,8 @@ const { WorkpadDropzone: errors } = ErrorStrings;
export const UploadDropzone: FC = ({ children }) => {
const notify = useNotifyService();
const uploadWorkpad = useImportWorkpad();
- const createWorkpad = useCreateWorkpad();
const [isDisabled, setIsDisabled] = useState(false);
- const onComplete = async (workpad?: CanvasWorkpad) => {
- if (!workpad) {
- setIsDisabled(false);
- return;
- }
-
- await createWorkpad(workpad);
- };
-
const onDrop = (files: FileList) => {
if (!files) {
return;
@@ -44,7 +33,7 @@ export const UploadDropzone: FC = ({ children }) => {
}
setIsDisabled(true);
- uploadWorkpad(files[0], onComplete);
+ uploadWorkpad(files[0], () => setIsDisabled(false));
};
return (
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
index e5d83039a87e..6d88691f2eab 100644
--- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
@@ -11,7 +11,8 @@ import { useSelector } from 'react-redux';
import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
import type { State } from '../../../../types';
import { usePlatformService } from '../../../services';
-import { useCloneWorkpad, useDownloadWorkpad } from '../hooks';
+import { useCloneWorkpad } from '../hooks';
+import { useDownloadWorkpad } from '../../hooks';
import { WorkpadTable as Component } from './workpad_table.component';
import { WorkpadsContext } from './my_workpads';
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
index 62d84adfc264..02b4ee61ea0c 100644
--- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
@@ -10,7 +10,8 @@ import { useSelector } from 'react-redux';
import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
import type { State } from '../../../../types';
-import { useDeleteWorkpads, useDownloadWorkpad } from '../hooks';
+import { useDeleteWorkpads } from '../hooks';
+import { useDownloadWorkpad } from '../../hooks';
import {
WorkpadTableTools as Component,
diff --git a/x-pack/plugins/canvas/public/components/hooks/index.tsx b/x-pack/plugins/canvas/public/components/hooks/index.tsx
new file mode 100644
index 000000000000..e420ab4cd698
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/hooks/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './workpad';
diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx
new file mode 100644
index 000000000000..50d527036560
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { useDownloadWorkpad, useDownloadRenderedWorkpad } from './use_download_workpad';
diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/use_download_workpad.ts b/x-pack/plugins/canvas/public/components/hooks/workpad/use_download_workpad.ts
new file mode 100644
index 000000000000..b688bb5a3b1a
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/hooks/workpad/use_download_workpad.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import fileSaver from 'file-saver';
+import { i18n } from '@kbn/i18n';
+import { useNotifyService, useWorkpadService } from '../../../services';
+import { CanvasWorkpad } from '../../../../types';
+import { CanvasRenderedWorkpad } from '../../../../shareable_runtime/types';
+
+const strings = {
+ getDownloadFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.downloadWorkpad.downloadFailureErrorMessage', {
+ defaultMessage: "Couldn't download workpad",
+ }),
+ getDownloadRenderedWorkpadFailureErrorMessage: () =>
+ i18n.translate(
+ 'xpack.canvas.error.downloadWorkpad.downloadRenderedWorkpadFailureErrorMessage',
+ {
+ defaultMessage: "Couldn't download rendered workpad",
+ }
+ ),
+};
+
+export const useDownloadWorkpad = () => {
+ const notifyService = useNotifyService();
+ const workpadService = useWorkpadService();
+ const download = useDownloadWorkpadBlob();
+
+ return useCallback(
+ async (workpadId: string) => {
+ try {
+ const workpad = await workpadService.get(workpadId);
+
+ download(workpad, `canvas-workpad-${workpad.name}-${workpad.id}`);
+ } catch (err) {
+ notifyService.error(err, { title: strings.getDownloadFailureErrorMessage() });
+ }
+ },
+ [workpadService, notifyService, download]
+ );
+};
+
+export const useDownloadRenderedWorkpad = () => {
+ const notifyService = useNotifyService();
+ const download = useDownloadWorkpadBlob();
+
+ return useCallback(
+ async (workpad: CanvasRenderedWorkpad) => {
+ try {
+ download(workpad, `canvas-embed-workpad-${workpad.name}-${workpad.id}`);
+ } catch (err) {
+ notifyService.error(err, {
+ title: strings.getDownloadRenderedWorkpadFailureErrorMessage(),
+ });
+ }
+ },
+ [notifyService, download]
+ );
+};
+
+const useDownloadWorkpadBlob = () => {
+ return useCallback((workpad: CanvasWorkpad | CanvasRenderedWorkpad, filename: string) => {
+ const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' });
+ fileSaver.saveAs(jsonBlob, `${filename}.json`);
+ }, []);
+};
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx
index be337a6dcf00..52e80c316c1e 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { FC } from 'react';
+import React, { FC, useCallback } from 'react';
import {
EuiText,
EuiSpacer,
@@ -24,35 +24,21 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { arrayBufferFetch } from '../../../../../common/lib/fetch';
-import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants';
import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types';
-import {
- downloadRenderedWorkpad,
- downloadRuntime,
- downloadZippedRuntime,
-} from '../../../../lib/download_workpad';
+import { useDownloadRenderedWorkpad } from '../../../hooks';
+import { useDownloadRuntime, useDownloadZippedRuntime } from './hooks';
import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants';
import { OnCloseFn } from '../share_menu.component';
import { WorkpadStep } from './workpad_step';
import { RuntimeStep } from './runtime_step';
import { SnippetsStep } from './snippets_step';
-import { useNotifyService, usePlatformService } from '../../../../services';
+import { useNotifyService } from '../../../../services';
const strings = {
getCopyShareConfigMessage: () =>
i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', {
defaultMessage: 'Copied share markup to clipboard',
}),
- getShareableZipErrorTitle: (workpadName: string) =>
- i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', {
- defaultMessage:
- "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.",
- values: {
- ZIP,
- workpadName,
- },
- }),
getUnknownExportErrorMessage: (type: string) =>
i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
defaultMessage: 'Unknown export type: {type}',
@@ -121,33 +107,33 @@ export const ShareWebsiteFlyout: FC = ({
renderedWorkpad,
}) => {
const notifyService = useNotifyService();
- const platformService = usePlatformService();
- const onCopy = () => {
- notifyService.info(strings.getCopyShareConfigMessage());
- };
- const onDownload = (type: 'share' | 'shareRuntime' | 'shareZip') => {
- switch (type) {
- case 'share':
- downloadRenderedWorkpad(renderedWorkpad);
- return;
- case 'shareRuntime':
- downloadRuntime(platformService.getBasePath());
- case 'shareZip':
- const basePath = platformService.getBasePath();
- arrayBufferFetch
- .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad))
- .then((blob) => downloadZippedRuntime(blob.data))
- .catch((err: Error) => {
- notifyService.error(err, {
- title: strings.getShareableZipErrorTitle(renderedWorkpad.name),
- });
- });
- return;
- default:
- throw new Error(strings.getUnknownExportErrorMessage(type));
- }
- };
+ const onCopy = useCallback(() => notifyService.info(strings.getCopyShareConfigMessage()), [
+ notifyService,
+ ]);
+
+ const downloadRenderedWorkpad = useDownloadRenderedWorkpad();
+ const downloadRuntime = useDownloadRuntime();
+ const downloadZippedRuntime = useDownloadZippedRuntime();
+
+ const onDownload = useCallback(
+ (type: 'share' | 'shareRuntime' | 'shareZip') => {
+ switch (type) {
+ case 'share':
+ downloadRenderedWorkpad(renderedWorkpad);
+ return;
+ case 'shareRuntime':
+ downloadRuntime();
+ return;
+ case 'shareZip':
+ downloadZippedRuntime(renderedWorkpad);
+ return;
+ default:
+ throw new Error(strings.getUnknownExportErrorMessage(type));
+ }
+ },
+ [downloadRenderedWorkpad, downloadRuntime, downloadZippedRuntime, renderedWorkpad]
+ );
const link = (
{
@@ -35,12 +34,6 @@ const getUnsupportedRenderers = (state: State) => {
return renderers;
};
-const mapStateToProps = (state: State) => ({
- renderedWorkpad: getRenderedWorkpad(state),
- unsupportedRenderers: getUnsupportedRenderers(state),
- workpad: getWorkpad(state),
-});
-
interface Props {
onClose: OnCloseFn;
renderedWorkpad: CanvasRenderedWorkpad;
@@ -48,14 +41,18 @@ interface Props {
workpad: CanvasWorkpad;
}
-export const ShareWebsiteFlyout = compose>(
- connect(mapStateToProps),
- withKibana,
- withProps(
- ({ unsupportedRenderers, renderedWorkpad, onClose, workpad }: Props): ComponentProps => ({
- renderedWorkpad,
- unsupportedRenderers,
- onClose,
- })
- )
-)(Component);
+export const ShareWebsiteFlyout: FC> = ({ onClose }) => {
+ const { renderedWorkpad, unsupportedRenderers } = useSelector((state: State) => ({
+ renderedWorkpad: getRenderedWorkpad(state),
+ unsupportedRenderers: getUnsupportedRenderers(state),
+ workpad: getWorkpad(state),
+ }));
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/index.ts
new file mode 100644
index 000000000000..a4243c9fff7e
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './use_download_runtime';
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/use_download_runtime.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/use_download_runtime.ts
new file mode 100644
index 000000000000..dc2e4ff685ca
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/hooks/use_download_runtime.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback } from 'react';
+import fileSaver from 'file-saver';
+import { i18n } from '@kbn/i18n';
+import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../../../../../common/lib/constants';
+import { ZIP } from '../../../../../../i18n/constants';
+
+import { usePlatformService, useNotifyService, useWorkpadService } from '../../../../../services';
+import { CanvasRenderedWorkpad } from '../../../../../../shareable_runtime/types';
+
+const strings = {
+ getDownloadRuntimeFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.downloadWorkpad.downloadRuntimeFailureErrorMessage', {
+ defaultMessage: "Couldn't download Shareable Runtime",
+ }),
+ getDownloadZippedRuntimeFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.downloadWorkpad.downloadZippedRuntimeFailureErrorMessage', {
+ defaultMessage: "Couldn't download ZIP file",
+ }),
+ getShareableZipErrorTitle: (workpadName: string) =>
+ i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', {
+ defaultMessage:
+ "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.",
+ values: {
+ ZIP,
+ workpadName,
+ },
+ }),
+};
+
+export const useDownloadRuntime = () => {
+ const platformService = usePlatformService();
+ const notifyService = useNotifyService();
+
+ const downloadRuntime = useCallback(() => {
+ try {
+ const path = `${platformService.getBasePath()}${API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD}`;
+ window.open(path);
+ return;
+ } catch (err) {
+ notifyService.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() });
+ }
+ }, [platformService, notifyService]);
+
+ return downloadRuntime;
+};
+
+export const useDownloadZippedRuntime = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+
+ const downloadZippedRuntime = useCallback(
+ (workpad: CanvasRenderedWorkpad) => {
+ const downloadZip = async () => {
+ try {
+ let runtimeZipBlob: Blob | undefined;
+ try {
+ runtimeZipBlob = await workpadService.getRuntimeZip(workpad);
+ } catch (err) {
+ notifyService.error(err, {
+ title: strings.getShareableZipErrorTitle(workpad.name),
+ });
+ }
+
+ if (runtimeZipBlob) {
+ fileSaver.saveAs(runtimeZipBlob, 'canvas-workpad-embed.zip');
+ }
+ } catch (err) {
+ notifyService.error(err, {
+ title: strings.getDownloadZippedRuntimeFailureErrorMessage(),
+ });
+ }
+ };
+
+ downloadZip();
+ },
+ [notifyService, workpadService]
+ );
+ return downloadZippedRuntime;
+};
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts
deleted file mode 100644
index f514f813599b..000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { connect } from 'react-redux';
-import { compose, withProps } from 'recompose';
-import { i18n } from '@kbn/i18n';
-
-import { CanvasWorkpad, State } from '../../../../types';
-import { downloadWorkpad } from '../../../lib/download_workpad';
-import { withServices, WithServicesProps } from '../../../services';
-import { getPages, getWorkpad } from '../../../state/selectors/workpad';
-import { Props as ComponentProps, ShareMenu as Component } from './share_menu.component';
-
-const strings = {
- getUnknownExportErrorMessage: (type: string) =>
- i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
- defaultMessage: 'Unknown export type: {type}',
- values: {
- type,
- },
- }),
-};
-
-const mapStateToProps = (state: State) => ({
- workpad: getWorkpad(state),
- pageCount: getPages(state).length,
-});
-
-interface Props {
- workpad: CanvasWorkpad;
- pageCount: number;
-}
-
-export const ShareMenu = compose(
- connect(mapStateToProps),
- withServices,
- withProps(
- ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => {
- const {
- reporting: { start: reporting },
- } = services;
-
- return {
- sharingServices: { reporting },
- sharingData: { workpad, pageCount },
- onExport: (type) => {
- switch (type) {
- case 'pdf':
- // notifications are automatically handled by the Reporting plugin
- break;
- case 'json':
- downloadWorkpad(workpad.id);
- return;
- default:
- throw new Error(strings.getUnknownExportErrorMessage(type));
- }
- },
- };
- }
- )
-)(Component);
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx
new file mode 100644
index 000000000000..0083ff1659c5
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useCallback } from 'react';
+import { useSelector } from 'react-redux';
+import { i18n } from '@kbn/i18n';
+import { State } from '../../../../types';
+import { useReportingService } from '../../../services';
+import { getPages, getWorkpad } from '../../../state/selectors/workpad';
+import { useDownloadWorkpad } from '../../hooks';
+import { ShareMenu as ShareMenuComponent } from './share_menu.component';
+
+const strings = {
+ getUnknownExportErrorMessage: (type: string) =>
+ i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
+ defaultMessage: 'Unknown export type: {type}',
+ values: {
+ type,
+ },
+ }),
+};
+
+export const ShareMenu: FC = () => {
+ const { workpad, pageCount } = useSelector((state: State) => ({
+ workpad: getWorkpad(state),
+ pageCount: getPages(state).length,
+ }));
+
+ const reportingService = useReportingService();
+ const downloadWorkpad = useDownloadWorkpad();
+
+ const sharingServices = {
+ reporting: reportingService.start,
+ };
+
+ const sharingData = {
+ workpad,
+ pageCount,
+ };
+
+ const onExport = useCallback(
+ (type: string) => {
+ switch (type) {
+ case 'pdf':
+ // notifications are automatically handled by the Reporting plugin
+ break;
+ case 'json':
+ downloadWorkpad(workpad.id);
+ return;
+ default:
+ throw new Error(strings.getUnknownExportErrorMessage(type));
+ }
+ },
+ [downloadWorkpad, workpad]
+ );
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/lib/download_workpad.ts b/x-pack/plugins/canvas/public/lib/download_workpad.ts
deleted file mode 100644
index a346de3322d0..000000000000
--- a/x-pack/plugins/canvas/public/lib/download_workpad.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import fileSaver from 'file-saver';
-import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants';
-import { ErrorStrings } from '../../i18n';
-
-// TODO: clint - convert this whole file to hooks
-import { pluginServices } from '../services';
-
-// @ts-expect-error untyped local
-import * as workpadService from './workpad_service';
-import { CanvasRenderedWorkpad } from '../../shareable_runtime/types';
-
-const { downloadWorkpad: strings } = ErrorStrings;
-
-export const downloadWorkpad = async (workpadId: string) => {
- try {
- const workpad = await workpadService.get(workpadId);
- const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' });
- fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`);
- } catch (err) {
- const notifyService = pluginServices.getServices().notify;
- notifyService.error(err, { title: strings.getDownloadFailureErrorMessage() });
- }
-};
-
-export const downloadRenderedWorkpad = async (renderedWorkpad: CanvasRenderedWorkpad) => {
- try {
- const jsonBlob = new Blob([JSON.stringify(renderedWorkpad)], { type: 'application/json' });
- fileSaver.saveAs(
- jsonBlob,
- `canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json`
- );
- } catch (err) {
- const notifyService = pluginServices.getServices().notify;
- notifyService.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() });
- }
-};
-
-export const downloadRuntime = async (basePath: string) => {
- try {
- const path = `${basePath}${API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD}`;
- window.open(path);
- return;
- } catch (err) {
- const notifyService = pluginServices.getServices().notify;
- notifyService.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() });
- }
-};
-
-export const downloadZippedRuntime = async (data: any) => {
- try {
- const zip = new Blob([data], { type: 'octet/stream' });
- fileSaver.saveAs(zip, 'canvas-workpad-embed.zip');
- } catch (err) {
- const notifyService = pluginServices.getServices().notify;
- notifyService.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() });
- }
-};
diff --git a/x-pack/plugins/canvas/public/lib/get_window.ts b/x-pack/plugins/canvas/public/lib/get_window.ts
index 82e57a8948ca..4fc8bcda5e66 100644
--- a/x-pack/plugins/canvas/public/lib/get_window.ts
+++ b/x-pack/plugins/canvas/public/lib/get_window.ts
@@ -10,14 +10,12 @@ const windowObj = {
location: null,
localStorage: {} as Window['localStorage'],
sessionStorage: {} as Window['sessionStorage'],
+ innerWidth: 0,
+ innerHeight: 0,
+ addEventListener: () => {},
+ removeEventListener: () => {},
};
-export const getWindow = ():
- | Window
- | {
- location: Location | null;
- localStorage: Window['localStorage'];
- sessionStorage: Window['sessionStorage'];
- } => {
+export const getWindow = (): Window | typeof windowObj => {
return typeof window === 'undefined' ? windowObj : window;
};
diff --git a/x-pack/plugins/canvas/public/lib/workpad_service.js b/x-pack/plugins/canvas/public/lib/workpad_service.js
deleted file mode 100644
index 20ad82860f1f..000000000000
--- a/x-pack/plugins/canvas/public/lib/workpad_service.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-// TODO: clint - move to workpad service.
-import {
- API_ROUTE_WORKPAD,
- API_ROUTE_WORKPAD_ASSETS,
- API_ROUTE_WORKPAD_STRUCTURES,
- DEFAULT_WORKPAD_CSS,
-} from '../../common/lib/constants';
-import { fetch } from '../../common/lib/fetch';
-import { pluginServices } from '../services';
-
-/*
- Remove any top level keys from the workpad which will be rejected by validation
-*/
-const validKeys = [
- '@created',
- '@timestamp',
- 'assets',
- 'colors',
- 'css',
- 'variables',
- 'height',
- 'id',
- 'isWriteable',
- 'name',
- 'page',
- 'pages',
- 'width',
-];
-
-const sanitizeWorkpad = function (workpad) {
- const workpadKeys = Object.keys(workpad);
-
- for (const key of workpadKeys) {
- if (!validKeys.includes(key)) {
- delete workpad[key];
- }
- }
-
- return workpad;
-};
-
-const getApiPath = function () {
- const platformService = pluginServices.getServices().platform;
- const basePath = platformService.getBasePath();
- return `${basePath}${API_ROUTE_WORKPAD}`;
-};
-
-const getApiPathStructures = function () {
- const platformService = pluginServices.getServices().platform;
- const basePath = platformService.getBasePath();
- return `${basePath}${API_ROUTE_WORKPAD_STRUCTURES}`;
-};
-
-const getApiPathAssets = function () {
- const platformService = pluginServices.getServices().platform;
- const basePath = platformService.getBasePath();
- return `${basePath}${API_ROUTE_WORKPAD_ASSETS}`;
-};
-
-export function create(workpad) {
- return fetch.post(getApiPath(), {
- ...sanitizeWorkpad({ ...workpad }),
- assets: workpad.assets || {},
- variables: workpad.variables || [],
- });
-}
-
-export async function createFromTemplate(templateId) {
- return fetch.post(getApiPath(), {
- templateId,
- });
-}
-
-export function get(workpadId) {
- return fetch.get(`${getApiPath()}/${workpadId}`).then(({ data: workpad }) => {
- // shim old workpads with new properties
- return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad };
- });
-}
-
-// TODO: I think this function is never used. Look into and remove the corresponding route as well
-export function update(id, workpad) {
- return fetch.put(`${getApiPath()}/${id}`, sanitizeWorkpad({ ...workpad }));
-}
-
-export function updateWorkpad(id, workpad) {
- return fetch.put(`${getApiPathStructures()}/${id}`, sanitizeWorkpad({ ...workpad }));
-}
-
-export function updateAssets(id, workpadAssets) {
- return fetch.put(`${getApiPathAssets()}/${id}`, workpadAssets);
-}
-
-export function remove(id) {
- return fetch.delete(`${getApiPath()}/${id}`);
-}
-
-export function find(searchTerm) {
- const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0;
-
- return fetch
- .get(`${getApiPath()}/find?name=${validSearchTerm ? searchTerm : ''}&perPage=10000`)
- .then(({ data: workpads }) => workpads);
-}
diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.test.tsx
new file mode 100644
index 000000000000..3ef93905f7e3
--- /dev/null
+++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.test.tsx
@@ -0,0 +1,200 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { renderHook } from '@testing-library/react-hooks';
+import { useWorkpadPersist } from './use_workpad_persist';
+
+const mockGetState = jest.fn();
+const mockUpdateWorkpad = jest.fn();
+const mockUpdateAssets = jest.fn();
+const mockUpdate = jest.fn();
+const mockNotifyError = jest.fn();
+
+// Mock the hooks and actions used by the UseWorkpad hook
+jest.mock('react-redux', () => ({
+ useSelector: (selector: any) => selector(mockGetState()),
+}));
+
+jest.mock('../../../services', () => ({
+ useWorkpadService: () => ({
+ updateWorkpad: mockUpdateWorkpad,
+ updateAssets: mockUpdateAssets,
+ update: mockUpdate,
+ }),
+ useNotifyService: () => ({
+ error: mockNotifyError,
+ }),
+}));
+
+describe('useWorkpadPersist', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ test('initial render does not persist state', () => {
+ const state = {
+ persistent: {
+ workpad: { some: 'workpad' },
+ },
+ assets: {
+ asset1: 'some asset',
+ asset2: 'other asset',
+ },
+ };
+
+ mockGetState.mockReturnValue(state);
+
+ renderHook(useWorkpadPersist);
+
+ expect(mockUpdateWorkpad).not.toBeCalled();
+ expect(mockUpdateAssets).not.toBeCalled();
+ expect(mockUpdate).not.toBeCalled();
+ });
+
+ test('changes to workpad cause a workpad update', () => {
+ const state = {
+ persistent: {
+ workpad: { some: 'workpad' },
+ },
+ assets: {
+ asset1: 'some asset',
+ asset2: 'other asset',
+ },
+ };
+
+ mockGetState.mockReturnValue(state);
+
+ const { rerender } = renderHook(useWorkpadPersist);
+
+ const newState = {
+ ...state,
+ persistent: {
+ workpad: { new: 'workpad' },
+ },
+ };
+ mockGetState.mockReturnValue(newState);
+
+ rerender();
+
+ expect(mockUpdateWorkpad).toHaveBeenCalled();
+ });
+
+ test('changes to assets cause an asset update', () => {
+ const state = {
+ persistent: {
+ workpad: { some: 'workpad' },
+ },
+ assets: {
+ asset1: 'some asset',
+ asset2: 'other asset',
+ },
+ };
+
+ mockGetState.mockReturnValue(state);
+
+ const { rerender } = renderHook(useWorkpadPersist);
+
+ const newState = {
+ ...state,
+ assets: {
+ asset1: 'some asset',
+ },
+ };
+ mockGetState.mockReturnValue(newState);
+
+ rerender();
+
+ expect(mockUpdateAssets).toHaveBeenCalled();
+ });
+
+ test('changes to both assets and workpad causes a full update', () => {
+ const state = {
+ persistent: {
+ workpad: { some: 'workpad' },
+ },
+ assets: {
+ asset1: 'some asset',
+ asset2: 'other asset',
+ },
+ };
+
+ mockGetState.mockReturnValue(state);
+
+ const { rerender } = renderHook(useWorkpadPersist);
+
+ const newState = {
+ persistent: {
+ workpad: { new: 'workpad' },
+ },
+ assets: {
+ asset1: 'some asset',
+ },
+ };
+ mockGetState.mockReturnValue(newState);
+
+ rerender();
+
+ expect(mockUpdate).toHaveBeenCalled();
+ });
+
+ test('non changes causes no updated', () => {
+ const state = {
+ persistent: {
+ workpad: { some: 'workpad' },
+ },
+ assets: {
+ asset1: 'some asset',
+ asset2: 'other asset',
+ },
+ };
+ mockGetState.mockReturnValue(state);
+
+ const { rerender } = renderHook(useWorkpadPersist);
+
+ rerender();
+
+ expect(mockUpdate).not.toHaveBeenCalled();
+ expect(mockUpdateWorkpad).not.toHaveBeenCalled();
+ expect(mockUpdateAssets).not.toHaveBeenCalled();
+ });
+
+ test('non write permissions causes no updates', () => {
+ const state = {
+ persistent: {
+ workpad: { some: 'workpad' },
+ },
+ assets: {
+ asset1: 'some asset',
+ asset2: 'other asset',
+ },
+ transient: {
+ canUserWrite: false,
+ },
+ };
+ mockGetState.mockReturnValue(state);
+
+ const { rerender } = renderHook(useWorkpadPersist);
+
+ const newState = {
+ persistent: {
+ workpad: { new: 'workpad value' },
+ },
+ assets: {
+ asset3: 'something',
+ },
+ transient: {
+ canUserWrite: false,
+ },
+ };
+ mockGetState.mockReturnValue(newState);
+
+ rerender();
+
+ expect(mockUpdate).not.toHaveBeenCalled();
+ expect(mockUpdateWorkpad).not.toHaveBeenCalled();
+ expect(mockUpdateAssets).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.ts
new file mode 100644
index 000000000000..62c83e041184
--- /dev/null
+++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_persist.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { useEffect, useCallback } from 'react';
+import { isEqual } from 'lodash';
+import usePrevious from 'react-use/lib/usePrevious';
+import { useSelector } from 'react-redux';
+import { i18n } from '@kbn/i18n';
+import { CanvasWorkpad, State } from '../../../../types';
+import { getWorkpad, getFullWorkpadPersisted } from '../../../state/selectors/workpad';
+import { canUserWrite } from '../../../state/selectors/app';
+import { getAssetIds } from '../../../state/selectors/assets';
+import { useWorkpadService, useNotifyService } from '../../../services';
+
+const strings = {
+ getSaveFailureTitle: () =>
+ i18n.translate('xpack.canvas.error.esPersist.saveFailureTitle', {
+ defaultMessage: "Couldn't save your changes to Elasticsearch",
+ }),
+ getTooLargeErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.esPersist.tooLargeErrorMessage', {
+ defaultMessage:
+ 'The server gave a response that the workpad data was too large. This usually means uploaded image assets that are too large for Kibana or a proxy. Try removing some assets in the asset manager.',
+ }),
+ getUpdateFailureTitle: () =>
+ i18n.translate('xpack.canvas.error.esPersist.updateFailureTitle', {
+ defaultMessage: "Couldn't update workpad",
+ }),
+};
+
+export const useWorkpadPersist = () => {
+ const service = useWorkpadService();
+ const notifyService = useNotifyService();
+ const notifyError = useCallback(
+ (err: any) => {
+ const statusCode = err.response && err.response.status;
+ switch (statusCode) {
+ case 400:
+ return notifyService.error(err.response, {
+ title: strings.getSaveFailureTitle(),
+ });
+ case 413:
+ return notifyService.error(strings.getTooLargeErrorMessage(), {
+ title: strings.getSaveFailureTitle(),
+ });
+ default:
+ return notifyService.error(err, {
+ title: strings.getUpdateFailureTitle(),
+ });
+ }
+ },
+ [notifyService]
+ );
+
+ // Watch for workpad state or workpad assets to change and then persist those changes
+ const [workpad, assetIds, fullWorkpad, canWrite]: [
+ CanvasWorkpad,
+ Array,
+ CanvasWorkpad,
+ boolean
+ ] = useSelector((state: State) => [
+ getWorkpad(state),
+ getAssetIds(state),
+ getFullWorkpadPersisted(state),
+ canUserWrite(state),
+ ]);
+
+ const previousWorkpad = usePrevious(workpad);
+ const previousAssetIds = usePrevious(assetIds);
+
+ const workpadChanged = previousWorkpad && workpad !== previousWorkpad;
+ const assetsChanged = previousAssetIds && !isEqual(assetIds, previousAssetIds);
+
+ useEffect(() => {
+ if (canWrite) {
+ if (workpadChanged && assetsChanged) {
+ service.update(workpad.id, fullWorkpad).catch(notifyError);
+ }
+ if (workpadChanged) {
+ service.updateWorkpad(workpad.id, workpad).catch(notifyError);
+ } else if (assetsChanged) {
+ service.updateAssets(workpad.id, fullWorkpad.assets).catch(notifyError);
+ }
+ }
+ }, [service, workpad, fullWorkpad, workpadChanged, assetsChanged, canWrite, notifyError]);
+};
diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx
index 95caba08517e..2c1ad4fcb6aa 100644
--- a/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx
+++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx
@@ -20,6 +20,7 @@ import { useWorkpad } from './hooks/use_workpad';
import { useRestoreHistory } from './hooks/use_restore_history';
import { useWorkpadHistory } from './hooks/use_workpad_history';
import { usePageSync } from './hooks/use_page_sync';
+import { useWorkpadPersist } from './hooks/use_workpad_persist';
import { WorkpadPageRouteProps, WorkpadRouteProps, WorkpadPageRouteParams } from '.';
import { WorkpadRoutingContextComponent } from './workpad_routing_context';
import { WorkpadPresentationHelper } from './workpad_presentation_helper';
@@ -88,6 +89,7 @@ export const WorkpadHistoryManager: FC = ({ children }) => {
useRestoreHistory();
useWorkpadHistory();
usePageSync();
+ useWorkpadPersist();
return <>{children}>;
};
diff --git a/x-pack/plugins/canvas/public/services/kibana/workpad.ts b/x-pack/plugins/canvas/public/services/kibana/workpad.ts
index 36ad1c568f9e..8609d5055cb8 100644
--- a/x-pack/plugins/canvas/public/services/kibana/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/kibana/workpad.ts
@@ -14,6 +14,9 @@ import {
API_ROUTE_WORKPAD,
DEFAULT_WORKPAD_CSS,
API_ROUTE_TEMPLATES,
+ API_ROUTE_WORKPAD_ASSETS,
+ API_ROUTE_WORKPAD_STRUCTURES,
+ API_ROUTE_SHAREABLE_ZIP,
} from '../../../common/lib/constants';
import { CanvasWorkpad } from '../../../types';
@@ -93,5 +96,25 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart,
remove: (id: string) => {
return coreStart.http.delete(`${getApiPath()}/${id}`);
},
+ update: (id, workpad) => {
+ return coreStart.http.put(`${getApiPath()}/${id}`, {
+ body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }),
+ });
+ },
+ updateWorkpad: (id, workpad) => {
+ return coreStart.http.put(`${API_ROUTE_WORKPAD_STRUCTURES}/${id}`, {
+ body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }),
+ });
+ },
+ updateAssets: (id, assets) => {
+ return coreStart.http.put(`${API_ROUTE_WORKPAD_ASSETS}/${id}`, {
+ body: JSON.stringify(assets),
+ });
+ },
+ getRuntimeZip: (workpad) => {
+ return coreStart.http.post(API_ROUTE_SHAREABLE_ZIP, {
+ body: JSON.stringify(workpad),
+ });
+ },
};
};
diff --git a/x-pack/plugins/canvas/public/services/legacy/context.tsx b/x-pack/plugins/canvas/public/services/legacy/context.tsx
index 2f472afd7d3c..fb30a9d418df 100644
--- a/x-pack/plugins/canvas/public/services/legacy/context.tsx
+++ b/x-pack/plugins/canvas/public/services/legacy/context.tsx
@@ -26,13 +26,14 @@ const defaultContextValue = {
search: {},
};
-const context = createContext(defaultContextValue as CanvasServices);
+export const ServicesContext = createContext(defaultContextValue as CanvasServices);
-export const useServices = () => useContext(context);
+export const useServices = () => useContext(ServicesContext);
export const useEmbeddablesService = () => useServices().embeddables;
export const useExpressionsService = () => useServices().expressions;
export const useNavLinkService = () => useServices().navLink;
export const useLabsService = () => useServices().labs;
+export const useReportingService = () => useServices().reporting;
export const withServices = (type: ComponentType) => {
const EnhancedType: FC = (props) =>
@@ -53,5 +54,5 @@ export const LegacyServicesProvider: FC<{
reporting: specifiedProviders.reporting.getService(),
labs: specifiedProviders.labs.getService(),
};
- return {children};
+ return {children};
};
diff --git a/x-pack/plugins/canvas/public/services/storybook/workpad.ts b/x-pack/plugins/canvas/public/services/storybook/workpad.ts
index a494f634141b..cdf4137e1d84 100644
--- a/x-pack/plugins/canvas/public/services/storybook/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/storybook/workpad.ts
@@ -97,4 +97,18 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({
action('workpadService.remove')(id);
return Promise.resolve();
},
+ update: (id, workpad) => {
+ action('worpadService.update')(workpad, id);
+ return Promise.resolve();
+ },
+ updateWorkpad: (id, workpad) => {
+ action('workpadService.updateWorkpad')(workpad, id);
+ return Promise.resolve();
+ },
+ updateAssets: (id, assets) => {
+ action('workpadService.updateAssets')(assets, id);
+ return Promise.resolve();
+ },
+ getRuntimeZip: (workpad) =>
+ Promise.resolve(new Blob([JSON.stringify(workpad)], { type: 'application/json' })),
});
diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
index eef7508e7c1e..2f2598563d49 100644
--- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
@@ -96,4 +96,9 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({
createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()),
find: findNoWorkpads(),
remove: (_id: string) => Promise.resolve(),
+ update: (id, workpad) => Promise.resolve(),
+ updateWorkpad: (id, workpad) => Promise.resolve(),
+ updateAssets: (id, assets) => Promise.resolve(),
+ getRuntimeZip: (workpad) =>
+ Promise.resolve(new Blob([JSON.stringify(workpad)], { type: 'application/json' })),
});
diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts
index 6b90cc346834..c0e948669647 100644
--- a/x-pack/plugins/canvas/public/services/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/workpad.ts
@@ -6,6 +6,7 @@
*/
import { CanvasWorkpad, CanvasTemplate } from '../../types';
+import { CanvasRenderedWorkpad } from '../../shareable_runtime/types';
export type FoundWorkpads = Array>;
export type FoundWorkpad = FoundWorkpads[number];
@@ -24,4 +25,8 @@ export interface CanvasWorkpadService {
find: (term: string) => Promise;
remove: (id: string) => Promise;
findTemplates: () => Promise;
+ update: (id: string, workpad: CanvasWorkpad) => Promise;
+ updateWorkpad: (id: string, workpad: CanvasWorkpad) => Promise;
+ updateAssets: (id: string, assets: CanvasWorkpad['assets']) => Promise;
+ getRuntimeZip: (workpad: CanvasRenderedWorkpad) => Promise;
}
diff --git a/x-pack/plugins/canvas/public/state/middleware/es_persist.js b/x-pack/plugins/canvas/public/state/middleware/es_persist.js
deleted file mode 100644
index 17d0c9649b91..000000000000
--- a/x-pack/plugins/canvas/public/state/middleware/es_persist.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { isEqual } from 'lodash';
-import { ErrorStrings } from '../../../i18n';
-import { getWorkpad, getFullWorkpadPersisted, getWorkpadPersisted } from '../selectors/workpad';
-import { getAssetIds } from '../selectors/assets';
-import { appReady } from '../actions/app';
-import { setWorkpad, setRefreshInterval, resetWorkpad } from '../actions/workpad';
-import { setAssets, resetAssets } from '../actions/assets';
-import * as transientActions from '../actions/transient';
-import * as resolvedArgsActions from '../actions/resolved_args';
-import { update, updateAssets, updateWorkpad } from '../../lib/workpad_service';
-import { pluginServices } from '../../services';
-import { canUserWrite } from '../selectors/app';
-
-const { esPersist: strings } = ErrorStrings;
-
-const workpadChanged = (before, after) => {
- const workpad = getWorkpad(before);
- return getWorkpad(after) !== workpad;
-};
-
-const assetsChanged = (before, after) => {
- const assets = getAssetIds(before);
- return !isEqual(assets, getAssetIds(after));
-};
-
-export const esPersistMiddleware = ({ getState }) => {
- // these are the actions we don't want to trigger a persist call
- const skippedActions = [
- appReady, // there's no need to resave the workpad once we've loaded it.
- resetWorkpad, // used for resetting the workpad in state
- setWorkpad, // used for loading and creating workpads
- setAssets, // used when loading assets
- resetAssets, // used when creating new workpads
- setRefreshInterval, // used to set refresh time interval which is a transient value
- ...Object.values(resolvedArgsActions), // no resolved args affect persisted values
- ...Object.values(transientActions), // no transient actions cause persisted state changes
- ].map((a) => a.toString());
-
- return (next) => (action) => {
- // if the action is in the skipped list, do not persist
- if (skippedActions.indexOf(action.type) >= 0) {
- return next(action);
- }
-
- // capture state before and after the action
- const curState = getState();
- next(action);
- const newState = getState();
-
- // skips the update request if user doesn't have write permissions
- if (!canUserWrite(newState)) {
- return;
- }
-
- const notifyError = (err) => {
- const statusCode = err.response && err.response.status;
- const notifyService = pluginServices.getServices().notify;
-
- switch (statusCode) {
- case 400:
- return notifyService.error(err.response, {
- title: strings.getSaveFailureTitle(),
- });
- case 413:
- return notifyService.error(strings.getTooLargeErrorMessage(), {
- title: strings.getSaveFailureTitle(),
- });
- default:
- return notifyService.error(err, {
- title: strings.getUpdateFailureTitle(),
- });
- }
- };
-
- const changedWorkpad = workpadChanged(curState, newState);
- const changedAssets = assetsChanged(curState, newState);
-
- if (changedWorkpad && changedAssets) {
- // if both the workpad and the assets changed, save it in its entirety to elasticsearch
- const persistedWorkpad = getFullWorkpadPersisted(getState());
- return update(persistedWorkpad.id, persistedWorkpad).catch(notifyError);
- } else if (changedWorkpad) {
- // if the workpad changed, save it to elasticsearch
- const persistedWorkpad = getWorkpadPersisted(getState());
- return updateWorkpad(persistedWorkpad.id, persistedWorkpad).catch(notifyError);
- } else if (changedAssets) {
- // if the assets changed, save it to elasticsearch
- const persistedWorkpad = getFullWorkpadPersisted(getState());
- return updateAssets(persistedWorkpad.id, persistedWorkpad.assets).catch(notifyError);
- }
- };
-};
diff --git a/x-pack/plugins/canvas/public/state/middleware/index.js b/x-pack/plugins/canvas/public/state/middleware/index.js
index 713232543fab..fbed2fbb3741 100644
--- a/x-pack/plugins/canvas/public/state/middleware/index.js
+++ b/x-pack/plugins/canvas/public/state/middleware/index.js
@@ -8,21 +8,13 @@
import { applyMiddleware, compose as reduxCompose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { getWindow } from '../../lib/get_window';
-import { esPersistMiddleware } from './es_persist';
import { inFlight } from './in_flight';
import { workpadUpdate } from './workpad_update';
import { elementStats } from './element_stats';
import { resolvedArgs } from './resolved_args';
const middlewares = [
- applyMiddleware(
- thunkMiddleware,
- elementStats,
- resolvedArgs,
- esPersistMiddleware,
- inFlight,
- workpadUpdate
- ),
+ applyMiddleware(thunkMiddleware, elementStats, resolvedArgs, inFlight, workpadUpdate),
];
// compose with redux devtools, if extension is installed
diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts
index e1cebeb65bd2..9cfccf3fc559 100644
--- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts
+++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts
@@ -7,6 +7,7 @@
import { get, omit } from 'lodash';
import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/common';
+import { CanvasRenderedWorkpad } from '../../../shareable_runtime/types';
import { append } from '../../lib/modify_path';
import { getAssets } from './assets';
import {
@@ -500,7 +501,7 @@ export function getRenderedWorkpad(state: State) {
return {
pages: renderedPages,
...rest,
- };
+ } as CanvasRenderedWorkpad;
}
export function getRenderedWorkpadExpressions(state: State) {
diff --git a/x-pack/plugins/canvas/shareable_runtime/types.ts b/x-pack/plugins/canvas/shareable_runtime/types.ts
index ac8f140b7f11..751fb3f79552 100644
--- a/x-pack/plugins/canvas/shareable_runtime/types.ts
+++ b/x-pack/plugins/canvas/shareable_runtime/types.ts
@@ -24,15 +24,14 @@ export interface CanvasRenderedElement {
* Represents a Page within a Canvas Workpad that is made up of ready-to-
* render Elements.
*/
-export interface CanvasRenderedPage extends Omit, 'groups'> {
+export interface CanvasRenderedPage extends Omit {
elements: CanvasRenderedElement[];
- groups: CanvasRenderedElement[][];
}
/**
* A Canvas Workpad made up of ready-to-render Elements.
*/
-export interface CanvasRenderedWorkpad extends Omit {
+export interface CanvasRenderedWorkpad extends Omit {
pages: CanvasRenderedPage[];
}
diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts
index a4ea3226b776..68481e27ae48 100644
--- a/x-pack/plugins/canvas/storybook/decorators/index.ts
+++ b/x-pack/plugins/canvas/storybook/decorators/index.ts
@@ -21,6 +21,6 @@ export const addDecorators = () => {
addDecorator(kibanaContextDecorator);
addDecorator(routerContextDecorator);
- addDecorator(servicesContextDecorator);
addDecorator(legacyContextDecorator());
+ addDecorator(servicesContextDecorator());
};
diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
index fbc3f140bffc..23dcc7b21a8b 100644
--- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
+++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
@@ -11,30 +11,35 @@ import { DecoratorFn } from '@storybook/react';
import { I18nProvider } from '@kbn/i18n/react';
import { PluginServiceRegistry } from '../../../../../src/plugins/presentation_util/public';
-import { pluginServices, LegacyServicesProvider } from '../../public/services';
-import { CanvasPluginServices } from '../../public/services';
+import { pluginServices, CanvasPluginServices } from '../../public/services';
import { pluginServiceProviders, StorybookParams } from '../../public/services/storybook';
+import { LegacyServicesProvider } from '../../public/services/legacy';
+import { startServices } from '../../public/services/legacy/stubs';
-export const servicesContextDecorator: DecoratorFn = (story: Function, storybook) => {
- if (process.env.JEST_WORKER_ID !== undefined) {
- storybook.args.useStaticData = true;
- }
-
+export const servicesContextDecorator = (): DecoratorFn => {
const pluginServiceRegistry = new PluginServiceRegistry(
pluginServiceProviders
);
- pluginServices.setRegistry(pluginServiceRegistry.start(storybook.args));
+ pluginServices.setRegistry(pluginServiceRegistry.start({}));
- const ContextProvider = pluginServices.getContextProvider();
+ return (story: Function, storybook) => {
+ if (process.env.JEST_WORKER_ID !== undefined) {
+ storybook.args.useStaticData = true;
+ }
- return (
-
- {story()}
-
- );
+ pluginServices.setRegistry(pluginServiceRegistry.start(storybook.args));
+ const ContextProvider = pluginServices.getContextProvider();
+
+ return (
+
+ {story()}
+
+ );
+ };
};
-export const legacyContextDecorator = () => (story: Function) => (
- {story()}
-);
+export const legacyContextDecorator = () => {
+ startServices();
+ return (story: Function) => {story()};
+};
diff --git a/x-pack/plugins/canvas/storybook/preview.ts b/x-pack/plugins/canvas/storybook/preview.ts
index 8eae76abaf41..040e1c3de149 100644
--- a/x-pack/plugins/canvas/storybook/preview.ts
+++ b/x-pack/plugins/canvas/storybook/preview.ts
@@ -7,14 +7,11 @@
import { addParameters } from '@storybook/react';
-import { startServices } from '../public/services/stubs';
import { addDecorators } from './decorators';
// Import Canvas CSS
import '../public/style/index.scss';
-startServices();
-
addDecorators();
addParameters({
controls: { hideNoControlsWarning: true },
diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts
index 6e27093379e3..cc42839ddfac 100644
--- a/x-pack/plugins/canvas/types/state.ts
+++ b/x-pack/plugins/canvas/types/state.ts
@@ -94,7 +94,7 @@ interface PersistentState {
export interface State {
app: StoreAppState;
- assets: { [assetKey: string]: AssetType | undefined };
+ assets: { [assetKey: string]: AssetType };
transient: TransientState;
persistent: PersistentState;
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
index ed35bfbe9784..d00ef999617e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
@@ -39,6 +39,7 @@ describe('EngineRouter', () => {
...mockEngineValues,
dataLoading: false,
engineNotFound: false,
+ isMetaEngine: false,
myRole: {},
};
const actions = {
@@ -175,14 +176,18 @@ describe('EngineRouter', () => {
});
it('renders a source engines view', () => {
- setMockValues({ ...values, myRole: { canViewMetaEngineSourceEngines: true } });
+ setMockValues({
+ ...values,
+ myRole: { canViewMetaEngineSourceEngines: true },
+ isMetaEngine: true,
+ });
const wrapper = shallow();
expect(wrapper.find(SourceEngines)).toHaveLength(1);
});
it('renders a crawler view', () => {
- setMockValues({ ...values, myRole: { canViewEngineCrawler: true } });
+ setMockValues({ ...values, myRole: { canViewEngineCrawler: true }, isMetaEngine: false });
const wrapper = shallow();
expect(wrapper.find(CrawlerRouter)).toHaveLength(1);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index 2d1bd32a0fff..8b0e6428babf 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -66,7 +66,7 @@ export const EngineRouter: React.FC = () => {
} = useValues(AppLogic);
const { engineName: engineNameFromUrl } = useParams() as { engineName: string };
- const { engineName, dataLoading, engineNotFound } = useValues(EngineLogic);
+ const { engineName, dataLoading, engineNotFound, isMetaEngine } = useValues(EngineLogic);
const { setEngineName, initializeEngine, pollEmptyEngine, stopPolling, clearEngine } = useActions(
EngineLogic
);
@@ -120,12 +120,12 @@ export const EngineRouter: React.FC = () => {
)}
- {canViewMetaEngineSourceEngines && (
+ {canViewMetaEngineSourceEngines && isMetaEngine && (
)}
- {canViewEngineCrawler && (
+ {canViewEngineCrawler && !isMetaEngine && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.scss
index 6ba90cba381c..677767c190f0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.scss
@@ -7,18 +7,14 @@
@include euiBreakpoint('m', 'l', 'xl') {
.kbnPageTemplateSolutionNav {
- position: relative;
- min-height: 100%;
-
- // Nested to override EUI specificity
- .betaNotificationSideNavItem {
- margin-top: $euiSizeL;
- }
+ display: flex;
+ flex-direction: column;
}
-
- .betaNotificationWrapper {
- position: absolute;
- bottom: 3px; // Without this 3px buffer, the popover won't render to the right
+ .euiSideNav__content {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
}
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.test.tsx
index 99b42b6f915e..4e4c7f4edbba 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.test.tsx
@@ -107,7 +107,6 @@ describe('appendBetaNotificationItem', () => {
{
id: 'beta',
name: '',
- className: 'betaNotificationSideNavItem',
renderItem: expect.any(Function),
},
],
@@ -118,7 +117,6 @@ describe('appendBetaNotificationItem', () => {
const SideNavItem = (mockSideNav.items[2] as any).renderItem;
const wrapper = shallow();
- expect(wrapper.hasClass('betaNotificationWrapper')).toBe(true);
expect(wrapper.find(BetaNotification)).toHaveLength(1);
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.tsx
index 46aa0a0af9e8..1f4c8328cc87 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/beta.tsx
@@ -98,12 +98,7 @@ export const appendBetaNotificationItem = (sideNav: KibanaPageTemplateProps['sol
sideNav.items.push({
id: 'beta',
name: '',
- className: 'betaNotificationSideNavItem',
- renderItem: () => (
-
-
-
- ),
+ renderItem: () => ,
});
}
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
index cf459171a808..0e56ee8f6724 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts
@@ -568,23 +568,23 @@ export const REDIRECT_INSECURE_ERROR_TEXT = i18n.translate(
}
);
-export const LICENSE_MODAL_TITLE = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.licenseModal.title',
+export const NON_PLATINUM_OAUTH_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthTitle',
{
defaultMessage: 'Configuring OAuth for Custom Search Applications',
}
);
-export const LICENSE_MODAL_DESCRIPTION = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.licenseModal.description',
+export const NON_PLATINUM_OAUTH_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthDescription',
{
defaultMessage:
'Configure an OAuth application for secure use of the Workplace Search Search API. Upgrade to a Platinum license to enable the Search API and create your OAuth application.',
}
);
-export const LICENSE_MODAL_LINK = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.licenseModal.link',
+export const NON_PLATINUM_OAUTH_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthLinkLabel',
{
defaultMessage: 'Explore Platinum features',
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts
index 2d15d6ce407b..e9ebc791622d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts
@@ -7,3 +7,4 @@
export { toSentenceSerial } from './to_sentence_serial';
export { getAsLocalDateTimeString } from './get_as_local_datetime_string';
+export { mimeType } from './mime_types';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.test.ts
new file mode 100644
index 000000000000..48b874484a0c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.test.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { mimeType } from './';
+
+describe('mimeType', () => {
+ it('should return correct mimeType when present', () => {
+ expect(mimeType('Image/gif')).toEqual('GIF');
+ });
+
+ it('should fall back gracefully when mimeType not present', () => {
+ expect(mimeType('NOPE')).toEqual('NOPE');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts
new file mode 100644
index 000000000000..f7664c90d461
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+const mimeTypes = {
+ 'application/iwork-keynote-sffkey': 'Keynote',
+ 'application/x-iwork-keynote-sffkey': 'Keynote',
+ 'application/iwork-numbers-sffnumbers': 'Numbers',
+ 'application/iwork-pages-sffpages': 'Pages',
+ 'application/json': 'JSON',
+ 'application/mp4': 'MP4',
+ 'application/msword': 'DOC',
+ 'application/octet-stream': 'Binary',
+ 'application/pdf': 'PDF',
+ 'application/rtf': 'RTF',
+ 'application/vnd.google-apps.audio': 'Google Audio',
+ 'application/vnd.google-apps.document': 'Google Doc',
+ 'application/vnd.google-apps.drawing': 'Google Drawing',
+ 'application/vnd.google-apps.file': 'Google Drive File',
+ 'application/vnd.google-apps.folder': 'Google Drive Folder',
+ 'application/vnd.google-apps.form': 'Google Form',
+ 'application/vnd.google-apps.fusiontable': 'Google Fusion Table',
+ 'application/vnd.google-apps.map': 'Google Map',
+ 'application/vnd.google-apps.photo': 'Google Photo',
+ 'application/vnd.google-apps.presentation': 'Google Slides',
+ 'application/vnd.google-apps.script': 'Google Script',
+ 'application/vnd.google-apps.sites': 'Google Site',
+ 'application/vnd.google-apps.spreadsheet': 'Google Sheet',
+ 'application/vnd.google-apps.unknown': 'Google Unknown',
+ 'application/vnd.google-apps.video': 'Google Video',
+ 'application/vnd.ms-excel': 'XLS',
+ 'application/vnd.ms-powerpoint': 'PPT',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPTX',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
+ 'application/xml': 'XML',
+ 'application/zip': 'ZIP',
+ 'image/gif': 'GIF',
+ 'image/jpeg': 'JPEG',
+ 'image/png': 'PNG',
+ 'image/svg+xml': 'SVG',
+ 'image/tiff': 'TIFF',
+ 'image/vnd.adobe.photoshop': 'PSD',
+ 'text/comma-separated-values': 'CSV',
+ 'text/css': 'CSS',
+ 'text/html': 'HTML',
+ 'text/plain': 'TXT',
+ 'video/quicktime': 'MOV',
+} as { [key: string]: string };
+
+export const mimeType = (type: string) => mimeTypes[type.toLowerCase()] || type;
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx
index 95a62b06515c..549faf1676a5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx
@@ -13,7 +13,7 @@ import { useValues } from 'kea';
import { isColorDark, hexToRgb } from '@elastic/eui';
import { DESCRIPTION_LABEL } from '../../../../constants';
-import { getAsLocalDateTimeString } from '../../../../utils';
+import { getAsLocalDateTimeString, mimeType } from '../../../../utils';
import { CustomSourceIcon } from './custom_source_icon';
import { DisplaySettingsLogic } from './display_settings_logic';
@@ -117,7 +117,7 @@ export const ExampleSearchResultGroup: React.FC = () => {
data-test-subj="MediaTypeField"
>
- {result[mediaTypeField]}
+ {mimeType(result[mediaTypeField] as string)}
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx
index b6aa180eb65d..46b8de678946 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx
@@ -13,7 +13,7 @@ import { useValues } from 'kea';
import { isColorDark, hexToRgb } from '@elastic/eui';
import { DESCRIPTION_LABEL } from '../../../../constants';
-import { getAsLocalDateTimeString } from '../../../../utils';
+import { getAsLocalDateTimeString, mimeType } from '../../../../utils';
import { CustomSourceIcon } from './custom_source_icon';
import { DisplaySettingsLogic } from './display_settings_logic';
@@ -108,7 +108,9 @@ export const ExampleStandoutResult: React.FC = () => {
})}
data-test-subj="MediaTypeField"
>
- {result[mediaTypeField]}
+
+ {mimeType(result[mediaTypeField] as string)}
+
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx
index a6a0fcda0dd6..6515c8740f7a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx
@@ -11,7 +11,7 @@ import { useActions, useValues } from 'kea';
import {
EuiColorPicker,
- EuiFlexGrid,
+ EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
@@ -76,7 +76,7 @@ export const SearchResults: React.FC = () => {
return (
<>
-
+
@@ -257,7 +257,7 @@ export const SearchResults: React.FC = () => {
-
+
>
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts
index 3e8322145dad..d642900aea16 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts
@@ -325,10 +325,11 @@ describe('SchemaLogic', () => {
});
it('handles duplicate', () => {
+ const onSchemaSetFormErrorsSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetFormErrors');
SchemaLogic.actions.onInitializeSchema(serverResponse);
SchemaLogic.actions.addNewField('foo', SchemaType.Number);
- expect(setErrorMessage).toHaveBeenCalledWith('New field already exists: foo.');
+ expect(onSchemaSetFormErrorsSpy).toHaveBeenCalledWith(['New field already exists: foo.']);
});
});
@@ -393,8 +394,10 @@ describe('SchemaLogic', () => {
it('handles error with message', async () => {
const onSchemaSetFormErrorsSpy = jest.spyOn(SchemaLogic.actions, 'onSchemaSetFormErrors');
- // We expect body.message to be a string[] when it is present
- http.post.mockReturnValue(Promise.reject({ body: { message: ['this is an error'] } }));
+ // We expect body.attributes.errors to be a string[] when it is present
+ http.post.mockReturnValue(
+ Promise.reject({ body: { attributes: { errors: ['this is an error'] } } })
+ );
SchemaLogic.actions.setServerField(schema, ADD);
await nextTick();
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts
index 7af074d412a6..f43be974102b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts
@@ -301,15 +301,15 @@ export const SchemaLogic = kea
>({
addNewField: ({ fieldName, newFieldType }) => {
if (fieldName in values.activeSchema) {
window.scrollTo(0, 0);
- setErrorMessage(
+ actions.onSchemaSetFormErrors([
i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.schema.newFieldExists.message',
{
defaultMessage: 'New field already exists: {fieldName}.',
values: { fieldName },
}
- )
- );
+ ),
+ ]);
} else {
const schema = cloneDeep(values.activeSchema);
schema[fieldName] = newFieldType;
@@ -350,8 +350,8 @@ export const SchemaLogic = kea>({
} catch (e) {
window.scrollTo(0, 0);
if (isAdding) {
- // We expect body.message to be a string[] for actions.onSchemaSetFormErrors
- const message: string[] = e?.body?.message || [defaultErrorMessage];
+ // We expect body.attributes.errors to be a string[] for actions.onSchemaSetFormErrors
+ const message: string[] = e?.body?.attributes?.errors || [defaultErrorMessage];
actions.onSchemaSetFormErrors(message);
} else {
flashAPIErrors(e);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
index 9f793fcd34fb..2a7dc2ebcc28 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
@@ -79,7 +79,7 @@ export const SourceRouter: React.FC = () => {
)}
{isCustomSource && (
-
+
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx
index f8c70d6bbba7..4d329ff357b8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx
@@ -14,7 +14,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiModal, EuiForm } from '@elastic/eui';
+import { EuiForm } from '@elastic/eui';
import { getPageDescription } from '../../../../test_helpers';
@@ -96,25 +96,55 @@ describe('OauthApplication', () => {
expect(wrapper.find(CredentialItem)).toHaveLength(2);
});
- it('renders license modal', () => {
- setMockValues({
- hasPlatinumLicense: false,
- oauthApplication,
+ describe('non-platinum license content', () => {
+ beforeEach(() => {
+ setMockValues({
+ hasPlatinumLicense: false,
+ oauthApplication,
+ });
});
- const wrapper = shallow();
-
- expect(wrapper.find(EuiModal)).toHaveLength(1);
- });
- it('closes license modal', () => {
- setMockValues({
- hasPlatinumLicense: false,
- oauthApplication,
+ it('renders pageTitle', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.prop('pageHeader').pageTitle).toMatchInlineSnapshot(`
+
+
+
+
+
+ Configuring OAuth for Custom Search Applications
+
+
+
+ `);
});
- const wrapper = shallow();
- wrapper.find(EuiModal).prop('onClose')();
- expect(wrapper.find(EuiModal)).toHaveLength(0);
+ it('renders description', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.prop('pageHeader').description).toMatchInlineSnapshot(`
+
+
+ Configure an OAuth application for secure use of the Workplace Search Search API. Upgrade to a Platinum license to enable the Search API and create your OAuth application.
+
+
+
+ Explore Platinum features
+
+
+ `);
+ });
});
it('handles conditional copy', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx
index ca8eadbcf75f..075d95f72603 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { FormEvent, useState } from 'react';
+import React, { FormEvent } from 'react';
import { useActions, useValues } from 'kea';
@@ -19,8 +19,6 @@ import {
EuiCode,
EuiSpacer,
EuiLink,
- EuiModal,
- EuiModalBody,
EuiTitle,
EuiText,
} from '@elastic/eui';
@@ -47,9 +45,9 @@ import {
REDIRECT_SECURE_ERROR_TEXT,
REDIRECT_URIS_LABEL,
SAVE_CHANGES_BUTTON,
- LICENSE_MODAL_TITLE,
- LICENSE_MODAL_DESCRIPTION,
- LICENSE_MODAL_LINK,
+ NON_PLATINUM_OAUTH_TITLE,
+ NON_PLATINUM_OAUTH_DESCRIPTION,
+ NON_PLATINUM_OAUTH_LINK,
} from '../../../constants';
import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes';
import { SettingsLogic } from '../settings_logic';
@@ -59,9 +57,6 @@ export const OauthApplication: React.FC = () => {
const { oauthApplication } = useValues(SettingsLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
- const [isLicenseModalVisible, setIsLicenseModalVisible] = useState(!hasPlatinumLicense);
- const closeLicenseModal = () => setIsLicenseModalVisible(false);
-
if (!oauthApplication) return null;
const persisted = !!(oauthApplication.uid && oauthApplication.secret);
@@ -91,38 +86,47 @@ export const OauthApplication: React.FC = () => {
updateOauthApplication();
};
- const licenseModal = (
-
-
-
-
-
-
- {LICENSE_MODAL_TITLE}
-
-
- {LICENSE_MODAL_DESCRIPTION}
-
-
- {LICENSE_MODAL_LINK}
-
-
-
-
+ const nonPlatinumTitle = (
+ <>
+
+
+
+ {NON_PLATINUM_OAUTH_TITLE}
+
+ >
+ );
+
+ const nonPlatinumDescription = (
+ <>
+ {NON_PLATINUM_OAUTH_DESCRIPTION}
+
+
+ {NON_PLATINUM_OAUTH_LINK}
+
+ >
);
return (
+
setOauthApplication({ ...oauthApplication, name: e.target.value })}
+ onChange={(e) =>
+ setOauthApplication({
+ ...oauthApplication,
+ name: e.target.value,
+ })
+ }
required
disabled={!hasPlatinumLicense}
/>
@@ -139,7 +143,10 @@ export const OauthApplication: React.FC = () => {
value={oauthApplication.redirectUri}
data-test-subj="RedirectURIsTextArea"
onChange={(e) =>
- setOauthApplication({ ...oauthApplication, redirectUri: e.target.value })
+ setOauthApplication({
+ ...oauthApplication,
+ redirectUri: e.target.value,
+ })
}
required
disabled={!hasPlatinumLicense}
@@ -152,7 +159,10 @@ export const OauthApplication: React.FC = () => {
checked={oauthApplication.confidential}
data-test-subj="ConfidentialToggle"
onChange={(e) =>
- setOauthApplication({ ...oauthApplication, confidential: e.target.checked })
+ setOauthApplication({
+ ...oauthApplication,
+ confidential: e.target.checked,
+ })
}
disabled={!hasPlatinumLicense}
/>
@@ -193,7 +203,6 @@ export const OauthApplication: React.FC = () => {
)}
- {isLicenseModalVisible && licenseModal}
);
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx
index 254885ea71b1..c0c425447e55 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx
@@ -42,7 +42,7 @@ const breadcrumbGetters: {
BASE_BREADCRUMB,
{
text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', {
- defaultMessage: 'Policies',
+ defaultMessage: 'Agent policies',
}),
},
],
@@ -50,7 +50,7 @@ const breadcrumbGetters: {
BASE_BREADCRUMB,
{
text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', {
- defaultMessage: 'Policies',
+ defaultMessage: 'Agent policies',
}),
},
],
@@ -59,7 +59,7 @@ const breadcrumbGetters: {
{
href: pagePathGetters.policies()[1],
text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', {
- defaultMessage: 'Policies',
+ defaultMessage: 'Agent policies',
}),
},
{ text: policyName },
@@ -69,7 +69,7 @@ const breadcrumbGetters: {
{
href: pagePathGetters.policies()[1],
text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', {
- defaultMessage: 'Policies',
+ defaultMessage: 'Agent policies',
}),
},
{
@@ -100,7 +100,7 @@ const breadcrumbGetters: {
{
href: pagePathGetters.policies()[1],
text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', {
- defaultMessage: 'Policies',
+ defaultMessage: 'Agent policies',
}),
},
{
diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx
index 7ad034b1cc05..dd15020adcc7 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx
@@ -49,7 +49,7 @@ export const DefaultLayout: React.FunctionComponent = ({
name: (
),
isSelected: section === 'agent_policies',
@@ -60,7 +60,7 @@ export const DefaultLayout: React.FunctionComponent = ({
name: (
),
isSelected: section === 'enrollment_tokens',
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx
index 1d7b44ceefb7..d6a6210bc867 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx
@@ -136,7 +136,6 @@ export const SearchAndFilterBar: React.FunctionComponent<{
onClick={() => setIsStatutsFilterOpen(!isStatusFilterOpen)}
isSelected={isStatusFilterOpen}
hasActiveFilters={selectedStatus.length > 0}
- numActiveFilters={selectedStatus.length}
disabled={agentPolicies.length === 0}
>
{
const fleetServerHost = settings?.item.fleet_server_hosts?.[0];
const output = outputsRequest.data?.items?.[0];
const esHost = output?.hosts?.[0];
+ const refreshOutputs = outputsRequest.resendRequest;
const installCommand = useMemo((): string => {
if (!serviceToken || !esHost) {
@@ -288,8 +289,8 @@ export const useFleetServerInstructions = (policyId?: string) => {
}, [notifications.toasts]);
const refresh = useCallback(() => {
- return Promise.all([outputsRequest.resendRequest(), refreshSettings()]);
- }, [outputsRequest, refreshSettings]);
+ return Promise.all([refreshOutputs(), refreshSettings()]);
+ }, [refreshOutputs, refreshSettings]);
const addFleetServerHost = useCallback(
async (host: string) => {
@@ -449,9 +450,9 @@ export const AddFleetServerHostStepContent = ({
setIsLoading(true);
if (validate(fleetServerHost)) {
await addFleetServerHost(fleetServerHost);
+ setCalloutHost(fleetServerHost);
+ setFleetServerHost('');
}
- setCalloutHost(fleetServerHost);
- setFleetServerHost('');
} finally {
setIsLoading(false);
}
@@ -481,10 +482,11 @@ export const AddFleetServerHostStepContent = ({
{
);
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 b91c4b60aa71..e129d7a4d5b4 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) {
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx
index f8e4c9994e57..8e900e625215 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx
@@ -22,20 +22,20 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType | 'view';
export const AssetTitleMap: Record = {
- dashboard: 'Dashboard',
- ilm_policy: 'ILM Policy',
- ingest_pipeline: 'Ingest Pipeline',
- transform: 'Transform',
- index_pattern: 'Index Pattern',
- index_template: 'Index Template',
- component_template: 'Component Template',
- search: 'Saved Search',
- visualization: 'Visualization',
- map: 'Map',
- data_stream_ilm_policy: 'Data Stream ILM Policy',
+ dashboard: 'Dashboards',
+ ilm_policy: 'ILM policies',
+ ingest_pipeline: 'Ingest pipelines',
+ transform: 'Transforms',
+ index_pattern: 'Index patterns',
+ index_template: 'Index templates',
+ component_template: 'Component templates',
+ search: 'Saved searches',
+ visualization: 'Visualizations',
+ map: 'Maps',
+ data_stream_ilm_policy: 'Data stream ILM policies',
lens: 'Lens',
- security_rule: 'Security Rule',
- ml_module: 'ML Module',
+ security_rule: 'Security rules',
+ ml_module: 'ML modules',
view: 'Views',
};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
index 91c6b68c6622..4a2d64fb8401 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
@@ -24,6 +24,8 @@ import { AssetTitleMap } from '../../../constants';
import { getHrefToObjectInKibanaApp, useStartServices } from '../../../../../hooks';
+import { KibanaAssetType } from '../../../../../types';
+
import type { AllowedAssetType, AssetSavedObject } from './types';
interface Props {
@@ -33,8 +35,12 @@ interface Props {
export const AssetsAccordion: FunctionComponent = ({ savedObjects, type }) => {
const { http } = useStartServices();
+
+ const isDashboard = type === KibanaAssetType.dashboard;
+
return (
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
index 79eea3441643..96e4071e9b46 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
import { stringify, parse } from 'query-string';
-import React, { memo, useCallback, useMemo, useState } from 'react';
+import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Redirect, useLocation, useHistory } from 'react-router-dom';
import type {
CriteriaWithPagination,
@@ -95,6 +95,21 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
const agentEnrollmentFlyoutExtension = useUIExtension(name, 'agent-enrollment-flyout');
+ // Handle the "add agent" link displayed in post-installation toast notifications in the case
+ // where a user is clicking the link while on the package policies listing page
+ useEffect(() => {
+ const unlisten = history.listen((location) => {
+ const params = new URLSearchParams(location.search);
+ const addAgentToPolicyId = params.get('addAgentToPolicyId');
+
+ if (addAgentToPolicyId) {
+ setFlyoutOpenForPolicyId(addAgentToPolicyId);
+ }
+ });
+
+ return () => unlisten();
+ }, [history]);
+
const handleTableOnChange = useCallback(
({ page }: CriteriaWithPagination) => {
setPagination({
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx
index 96fab27a5505..adc6ba44dbb1 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx
@@ -22,12 +22,14 @@ import {
interface Props {
agentPolicyId?: string;
selectedApiKeyId?: string;
+ initialAuthenticationSettingsOpen?: boolean;
onKeyChange: (key?: string) => void;
}
export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({
agentPolicyId,
selectedApiKeyId,
+ initialAuthenticationSettingsOpen = false,
onKeyChange,
}) => {
const { notifications } = useStartServices();
@@ -35,7 +37,9 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent = ({
[]
);
const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false);
- const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(false);
+ const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState(
+ initialAuthenticationSettingsOpen
+ );
const onCreateEnrollmentTokenClick = async () => {
setIsLoadingEnrollmentKey(true);
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
index d16be0d8b97e..d2e7c4089e88 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
@@ -50,6 +50,7 @@ jest.mock('./steps', () => {
AgentPolicySelectionStep: jest.fn(),
AgentEnrollmentKeySelectionStep: jest.fn(),
ViewDataStep: jest.fn(),
+ DownloadStep: jest.fn(),
};
});
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx
index 8eeb5fac4b0d..6cffa39628d9 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx
@@ -9,15 +9,24 @@ import React, { useCallback, useMemo } from 'react';
import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import semver from 'semver';
import type { AgentPolicy, PackagePolicy } from '../../types';
-import { sendGetOneAgentPolicy } from '../../hooks';
+import { sendGetOneAgentPolicy, useKibanaVersion } from '../../hooks';
import { FLEET_SERVER_PACKAGE } from '../../constants';
import { EnrollmentStepAgentPolicy } from './agent_policy_selection';
import { AdvancedAgentAuthenticationSettings } from './advanced_agent_authentication_settings';
export const DownloadStep = () => {
+ const kibanaVersion = useKibanaVersion();
+ const kibanaVersionURLString = useMemo(
+ () =>
+ `${semver.major(kibanaVersion)}-${semver.minor(kibanaVersion)}-${semver.patch(
+ kibanaVersion
+ )}`,
+ [kibanaVersion]
+ );
return {
title: i18n.translate('xpack.fleet.agentEnrollment.stepDownloadAgentTitle', {
defaultMessage: 'Download the Elastic Agent to your host',
@@ -30,9 +39,16 @@ export const DownloadStep = () => {
defaultMessage="Fleet Server runs on an Elastic Agent. You can download the Elastic Agent binaries and verification signatures from Elastic’s download page."
/>
+
+
+
+
{
return {
title: i18n.translate('xpack.fleet.agentEnrollment.stepConfigurePolicyAuthenticationTitle', {
- defaultMessage: 'Configure agent authentication',
+ defaultMessage: 'Select enrollment token',
}),
children: (
<>
{agentPolicy.name},
}}
@@ -138,6 +154,7 @@ export const AgentEnrollmentKeySelectionStep = ({
>
diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx
index 819fa03e2b6d..074a1c40bdb1 100644
--- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx
+++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx
@@ -33,10 +33,10 @@ const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo((
return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? (
-
+
diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx
index 23754571c5bc..e784ff1ac6ad 100644
--- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx
+++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx
@@ -71,13 +71,13 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => {
title={
),
@@ -88,12 +88,16 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => {
+
{
-
+
diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx
index f5b76de46e3a..6b9b441551a5 100644
--- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx
+++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx
@@ -31,8 +31,8 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }
@@ -50,13 +50,13 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }
>
),
blogPostLink: (
diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
index 9743135d5f1c..7b0a300ac9dc 100644
--- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
+++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
@@ -28,6 +28,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
const { getHref } = useLink();
const hasWriteCapabilities = useCapabilities().write;
const refreshAgentPolicy = useAgentPolicyRefresh();
+ const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false);
const onEnrollmentFlyoutClose = useMemo(() => {
return () => setIsEnrollmentFlyoutOpen(false);
@@ -48,7 +49,10 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
// ,
setIsEnrollmentFlyoutOpen(true)}
+ onClick={() => {
+ setIsActionsMenuOpen(false);
+ setIsEnrollmentFlyoutOpen(true);
+ }}
key="addAgent"
>
)}
-
+ setIsActionsMenuOpen(isOpen)}
+ />
>
);
};
diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts
index 9b3aefa488f7..f4a347c2ab3f 100644
--- a/x-pack/plugins/fleet/public/plugin.ts
+++ b/x-pack/plugins/fleet/public/plugin.ts
@@ -32,7 +32,7 @@ import type { CheckPermissionsResponse, PostIngestSetupResponse } from '../commo
import type { FleetConfigType } from '../common/types';
-import { CUSTOM_LOGS_INTEGRATION_NAME, FLEET_BASE_PATH } from './constants';
+import { CUSTOM_LOGS_INTEGRATION_NAME, INTEGRATIONS_BASE_PATH } from './constants';
import { licenseService } from './hooks';
import { setHttpClient } from './hooks/use_request';
import { createPackageSearchProvider } from './search_provider';
@@ -183,14 +183,14 @@ export class FleetPlugin implements Plugin {
id: 'test-test',
score: 80,
title: 'test',
- type: 'package',
+ type: 'integration',
url: {
path: 'undefined#/detail/test-test/overview',
prependBasePath: false,
@@ -100,7 +100,7 @@ describe('Package search provider', () => {
id: 'test1-test1',
score: 80,
title: 'test1',
- type: 'package',
+ type: 'integration',
url: {
path: 'undefined#/detail/test1-test1/overview',
prependBasePath: false,
@@ -173,7 +173,7 @@ describe('Package search provider', () => {
id: 'test-test',
score: 80,
title: 'test',
- type: 'package',
+ type: 'integration',
url: {
path: 'undefined#/detail/test-test/overview',
prependBasePath: false,
@@ -209,7 +209,7 @@ describe('Package search provider', () => {
expect(sendGetPackages).toHaveBeenCalledTimes(0);
});
- test('with packages tag, with no search term', () => {
+ test('with integration tag, with no search term', () => {
getTestScheduler().run(({ hot, expectObservable }) => {
mockSendGetPackages.mockReturnValue(
hot('--(a|)', { a: { data: { response: testResponse } } })
@@ -220,7 +220,7 @@ describe('Package search provider', () => {
const packageSearchProvider = createPackageSearchProvider(setupMock);
expectObservable(
packageSearchProvider.find(
- { types: ['package'] },
+ { types: ['integration'] },
{ aborted$: NEVER, maxResults: 100, preference: '' }
)
).toBe('--(a|)', {
@@ -229,7 +229,7 @@ describe('Package search provider', () => {
id: 'test-test',
score: 80,
title: 'test',
- type: 'package',
+ type: 'integration',
url: {
path: 'undefined#/detail/test-test/overview',
prependBasePath: false,
@@ -239,7 +239,7 @@ describe('Package search provider', () => {
id: 'test1-test1',
score: 80,
title: 'test1',
- type: 'package',
+ type: 'integration',
url: {
path: 'undefined#/detail/test1-test1/overview',
prependBasePath: false,
@@ -252,7 +252,7 @@ describe('Package search provider', () => {
expect(sendGetPackages).toHaveBeenCalledTimes(1);
});
- test('with packages tag, with search term', () => {
+ test('with integration tag, with search term', () => {
getTestScheduler().run(({ hot, expectObservable }) => {
mockSendGetPackages.mockReturnValue(
hot('--(a|)', { a: { data: { response: testResponse } } })
@@ -263,7 +263,7 @@ describe('Package search provider', () => {
const packageSearchProvider = createPackageSearchProvider(setupMock);
expectObservable(
packageSearchProvider.find(
- { term: 'test1', types: ['package'] },
+ { term: 'test1', types: ['integration'] },
{ aborted$: NEVER, maxResults: 100, preference: '' }
)
).toBe('--(a|)', {
@@ -272,7 +272,7 @@ describe('Package search provider', () => {
id: 'test1-test1',
score: 80,
title: 'test1',
- type: 'package',
+ type: 'integration',
url: {
path: 'undefined#/detail/test1-test1/overview',
prependBasePath: false,
diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts
index 56e08ecad29f..a8b46a3dc0f0 100644
--- a/x-pack/plugins/fleet/public/search_provider.ts
+++ b/x-pack/plugins/fleet/public/search_provider.ts
@@ -21,7 +21,7 @@ import { sendGetPackages } from './hooks';
import type { GetPackagesResponse } from './types';
import { pagePathGetters } from './constants';
-const packageType = 'package';
+const packageType = 'integration';
const createPackages$ = () =>
from(sendGetPackages()).pipe(
@@ -70,7 +70,7 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult
};
return {
- id: 'packages',
+ id: 'integrations',
getSearchableTypes: () => [packageType],
find: ({ term, types }, { maxResults, aborted$ }) => {
if (types?.includes(packageType) === false) {
diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts
index 1f8f54261c72..0ab102d91cd4 100644
--- a/x-pack/plugins/fleet/server/plugin.ts
+++ b/x-pack/plugins/fleet/server/plugin.ts
@@ -15,7 +15,6 @@ import type {
PluginInitializerContext,
SavedObjectsServiceStart,
HttpServiceSetup,
- SavedObjectsClientContract,
RequestHandlerContext,
KibanaRequest,
} from 'kibana/server';
@@ -30,12 +29,7 @@ import type {
} from '../../encrypted_saved_objects/server';
import type { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
-import type {
- EsAssetReference,
- FleetConfigType,
- NewPackagePolicy,
- UpdatePackagePolicy,
-} from '../common';
+import type { FleetConfigType, NewPackagePolicy, UpdatePackagePolicy } from '../common';
import { INTEGRATIONS_PLUGIN_ID } from '../common';
import type { CloudSetup } from '../../cloud/server';
@@ -224,7 +218,7 @@ export class FleetPlugin
if (deps.features) {
deps.features.registerKibanaFeature({
id: PLUGIN_ID,
- name: 'Fleet',
+ name: 'Fleet and Integrations',
category: DEFAULT_APP_CATEGORIES.management,
app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'],
catalogue: ['fleet'],
@@ -309,13 +303,7 @@ export class FleetPlugin
}),
esIndexPatternService: new ESIndexPatternSavedObjectService(),
packageService: {
- getInstalledEsAssetReferences: async (
- savedObjectsClient: SavedObjectsClientContract,
- pkgName: string
- ): Promise => {
- const installation = await getInstallation({ savedObjectsClient, pkgName });
- return installation?.installed_es || [];
- },
+ getInstallation,
},
agentService: {
getAgent: getAgentById,
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index cff70737be6e..830298331631 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 = [
diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts
index f82415987e5a..f4355320c5a6 100644
--- a/x-pack/plugins/fleet/server/services/index.ts
+++ b/x-pack/plugins/fleet/server/services/index.ts
@@ -8,11 +8,12 @@
import type { KibanaRequest } from 'kibana/server';
import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server';
-import type { AgentStatus, Agent, EsAssetReference } from '../types';
+import type { AgentStatus, Agent } from '../types';
import type { getAgentById, getAgentsByKuery } from './agents';
import type { agentPolicyService } from './agent_policy';
import * as settingsService from './settings';
+import type { getInstallation } from './epm/packages';
export { ESIndexPatternSavedObjectService } from './es_index_pattern';
export { getRegistryUrl } from './epm/registry/registry_url';
@@ -33,10 +34,7 @@ export interface ESIndexPatternService {
*/
export interface PackageService {
- getInstalledEsAssetReferences(
- savedObjectsClient: SavedObjectsClientContract,
- pkgName: string
- ): Promise;
+ getInstallation: typeof getInstallation;
}
/**
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts
index 9b3e9b7a5736..cf06136c487e 100644
--- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts
@@ -14,7 +14,10 @@ import type { AgentPolicy, NewPackagePolicy, Output } from '../types';
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants';
-import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration';
+import {
+ ensurePreconfiguredPackagesAndPolicies,
+ comparePreconfiguredPolicyToCurrent,
+} from './preconfiguration';
jest.mock('./agent_policy_update');
@@ -279,3 +282,75 @@ describe('policy preconfiguration', () => {
expect(policiesB[0].updated_at).toEqual(policiesA[0].updated_at);
});
});
+
+describe('comparePreconfiguredPolicyToCurrent', () => {
+ const baseConfig = {
+ name: 'Test policy',
+ namespace: 'default',
+ description: 'This is a test policy',
+ id: 'test-id',
+ unenroll_timeout: 60,
+ package_policies: [
+ {
+ package: { name: 'test_package' },
+ name: 'Test package',
+ },
+ ],
+ };
+
+ const basePackagePolicy: AgentPolicy = {
+ id: 'test-id',
+ namespace: 'default',
+ monitoring_enabled: ['logs', 'metrics'],
+ name: 'Test policy',
+ description: 'This is a test policy',
+ unenroll_timeout: 60,
+ is_preconfigured: true,
+ status: 'active',
+ is_managed: true,
+ revision: 1,
+ updated_at: '2021-07-07T16:29:55.144Z',
+ updated_by: 'system',
+ package_policies: [
+ {
+ package: { name: 'test_package', title: 'Test package', version: '1.0.0' },
+ name: 'Test package',
+ namespace: 'default',
+ enabled: true,
+ id: 'test-package-id',
+ revision: 1,
+ updated_at: '2021-07-07T16:29:55.144Z',
+ updated_by: 'system',
+ created_at: '2021-07-07T16:29:55.144Z',
+ created_by: 'system',
+ inputs: [],
+ policy_id: 'abc123',
+ output_id: 'default',
+ },
+ ],
+ };
+
+ it('should return hasChanged when a top-level policy field changes', () => {
+ const { hasChanged } = comparePreconfiguredPolicyToCurrent(
+ { ...baseConfig, unenroll_timeout: 120 },
+ basePackagePolicy
+ );
+ expect(hasChanged).toBe(true);
+ });
+
+ it('should not return hasChanged when no top-level fields change', () => {
+ const { hasChanged } = comparePreconfiguredPolicyToCurrent(
+ {
+ ...baseConfig,
+ package_policies: [
+ {
+ package: { name: 'different_package' },
+ name: 'Different package',
+ },
+ ],
+ },
+ basePackagePolicy
+ );
+ expect(hasChanged).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts
index f153ed3e68d3..0e24871628dc 100644
--- a/x-pack/plugins/fleet/server/services/preconfiguration.ts
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts
@@ -7,7 +7,7 @@
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
import { i18n } from '@kbn/i18n';
-import { groupBy, omit, isEqual } from 'lodash';
+import { groupBy, omit, pick, isEqual } from 'lodash';
import type {
NewPackagePolicy,
@@ -142,14 +142,16 @@ export async function ensurePreconfiguredPackagesAndPolicies(
if (!created) {
if (!policy?.is_managed) return { created, policy };
- const configTopLevelFields = omit(preconfiguredAgentPolicy, 'package_policies', 'id');
- const currentTopLevelFields = omit(policy, 'package_policies');
- if (!isEqual(configTopLevelFields, currentTopLevelFields)) {
+ const { hasChanged, fields } = comparePreconfiguredPolicyToCurrent(
+ preconfiguredAgentPolicy,
+ policy
+ );
+ if (hasChanged) {
const updatedPolicy = await agentPolicyService.update(
soClient,
esClient,
String(preconfiguredAgentPolicy.id),
- configTopLevelFields
+ fields
);
return { created, policy: updatedPolicy };
}
@@ -243,6 +245,19 @@ export async function ensurePreconfiguredPackagesAndPolicies(
};
}
+export function comparePreconfiguredPolicyToCurrent(
+ policyFromConfig: PreconfiguredAgentPolicy,
+ currentPolicy: AgentPolicy
+) {
+ const configTopLevelFields = omit(policyFromConfig, 'package_policies', 'id');
+ const currentTopLevelFields = pick(currentPolicy, ...Object.keys(configTopLevelFields));
+
+ return {
+ hasChanged: !isEqual(configTopLevelFields, currentTopLevelFields),
+ fields: configTopLevelFields,
+ };
+}
+
async function addPreconfiguredPolicyPackages(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts
index 4cdeb678c432..9d85316d978e 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts
@@ -7,7 +7,7 @@
import { isEqual } from 'lodash';
import createContainer from 'constate';
-import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import useSetState from 'react-use/lib/useSetState';
import { esQuery } from '../../../../../../../src/plugins/data/public';
@@ -66,10 +66,9 @@ export function useLogStream({
const prevStartTimestamp = usePrevious(startTimestamp);
const prevEndTimestamp = usePrevious(endTimestamp);
- const cachedQuery = useRef(query);
-
+ const [cachedQuery, setCachedQuery] = useState(query);
if (!isEqual(query, cachedQuery)) {
- cachedQuery.current = query;
+ setCachedQuery(query);
}
useEffect(() => {
@@ -89,7 +88,7 @@ export function useLogStream({
sourceId,
startTimestamp,
endTimestamp,
- query: cachedQuery.current,
+ query: cachedQuery,
columnOverrides: columns,
}),
[columns, endTimestamp, cachedQuery, sourceId, startTimestamp]
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 9806cdaad637..445df21a6067 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))\`
`,
+ }),
},
};
diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.ts
index 5525f6b16b31..54696e672ccb 100644
--- a/x-pack/plugins/lens/public/shared_components/debounced_value.ts
+++ b/x-pack/plugins/lens/public/shared_components/debounced_value.ts
@@ -30,12 +30,20 @@ export const useDebouncedValue = (
// Save the initial value
const initialValue = useRef(value);
+ const flushChangesTimeout = useRef();
+
const onChangeDebounced = useMemo(() => {
const callback = debounce((val: T) => {
onChange(val);
- unflushedChanges.current = false;
+ // do not reset unflushed flag right away, wait a bit for upstream to pick it up
+ flushChangesTimeout.current = setTimeout(() => {
+ unflushedChanges.current = false;
+ }, 256);
}, 256);
return (val: T) => {
+ if (flushChangesTimeout.current) {
+ clearTimeout(flushChangesTimeout.current);
+ }
unflushedChanges.current = true;
callback(val);
};
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index 04533f6c914e..8f7fe4601739 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -540,7 +540,10 @@ export function DimensionEditor(
(yAxisConfig) => yAxisConfig.forAccessor === accessor
);
if (existingIndex !== -1) {
- newYAxisConfigs[existingIndex].axisMode = newMode;
+ newYAxisConfigs[existingIndex] = {
+ ...newYAxisConfigs[existingIndex],
+ axisMode: newMode,
+ };
} else {
newYAxisConfigs.push({
forAccessor: accessor,
@@ -625,9 +628,9 @@ const ColorPicker = ({
const existingIndex = newYConfigs.findIndex((yConfig) => yConfig.forAccessor === accessor);
if (existingIndex !== -1) {
if (text === '') {
- delete newYConfigs[existingIndex].color;
+ newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], color: undefined };
} else {
- newYConfigs[existingIndex].color = output.hex;
+ newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], color: output.hex };
}
} else {
newYConfigs.push({
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
index 66f0e0c4a951..ce4fcd8f1256 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx
@@ -134,7 +134,7 @@ export class DrawControl extends Component {
},
paint: {
'text-color': '#fbb03b',
- 'text-halo-color': 'rgba(255, 255, 255, 1)',
+ 'text-halo-color': 'rgba(0, 0, 0, 1)',
'text-halo-width': 2,
},
});
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx
index 9f2b99b9c41a..7b408df3a813 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_feature_control/draw_feature_control.tsx
@@ -6,7 +6,7 @@
*/
import React, { Component } from 'react';
-import { Map as MbMap, Point as MbPoint } from 'mapbox-gl';
+import { Map as MbMap, Point as MbPoint } from '@kbn/mapbox-gl';
// @ts-expect-error
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { Feature, Geometry, Position } from 'geojson';
diff --git a/x-pack/plugins/metrics_entities/README.md b/x-pack/plugins/metrics_entities/README.md
index 6c711ce4fed8..71ac2730f383 100755
--- a/x-pack/plugins/metrics_entities/README.md
+++ b/x-pack/plugins/metrics_entities/README.md
@@ -2,7 +2,7 @@
This is the metrics and entities plugin where you add can add transforms for your project
and group those transforms into modules. You can also re-use existing transforms in your
-modules as well.
+newly created modules as well.
## Turn on experimental flags
During at least phase 1 of this development, please add these to your `kibana.dev.yml` file to turn on the feature:
@@ -309,16 +309,14 @@ are notes during the phased approach. As we approach production and the feature
left over TODO's in the code base.
- Add these properties to the route which are:
- - disable_transforms/exclude flag to exclude 1 or more transforms within a module,
- - pipeline flag,
- - Change the REST routes on post to change the indexes for whichever indexes you want
- - Unit tests to ensure the data of the mapping.json includes the correct fields such as
- _meta, at least one alias, a mapping section, etc...
- - Add text/keyword and other things to the mappings (not just keyword maybe?) ... At least review the mappings one more time
- - Add a sort of @timestamp to the output destination indexes?
- - Add the REST Kibana security based tags if needed and push those to any plugins using this plugin. Something like: tags: ['access:metricsEntities-read'] and ['access:metricsEntities-all'],
- - Add schema validation choosing some schema library (io-ts or Kibana Schema or ... )
- - Add unit tests
- - Add e2e tests
- - Move ui code into this plugin from security_solutions? (maybe?)
- - UI code could be within `kibana/packages` instead of in here directly and I think we will be better off.
+ - disable_transforms/exclude flag to exclude 1 or more transforms within a module
+ - pipeline flag
+ - Change the REST routes on post to change the indexes for whichever indexes you want
+- Unit tests to ensure the data of the mapping.json includes the correct fields such as _meta, at least one alias, a mapping section, etc...
+- Add text/keyword and other things to the mappings (not just keyword maybe?) ... At least review the mappings one more time
+- Add a sort of @timestamp to the output destination indexes?
+- Add the REST Kibana security based tags if needed and push those to any plugins using this plugin. Something like: tags: ['access:metricsEntities-read'] and ['access:metricsEntities-all'],
+- Add schema validation choosing some schema library (io-ts or Kibana Schema or ... )
+- Add unit tests
+- Add e2e tests
+- Any UI code should not end up here. There is none right now, but all UI code should be within a kbn package or security_solutions
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
index 9dcd6abb432b..47684ee307e9 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
@@ -82,6 +82,7 @@ export interface UseIndexDataReturnType
| 'resultsField'
> {
renderCellValue: RenderCellValue;
+ indexPatternFields?: string[];
}
export interface UseDataGridReturnType {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index 669b95cbaeb8..bac6b0b9274f 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -102,7 +102,7 @@ export enum INDEX_STATUS {
export interface FieldSelectionItem {
name: string;
- mappings_types: string[];
+ mappings_types?: string[];
is_included: boolean;
is_required: boolean;
feature_type?: string;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index cc01a8c3f940..47f7c2621802 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -115,6 +115,7 @@ export const ConfigurationStepForm: FC