throw new Error('Not implemented.');
},
}),
- },
+ }),
...partialDeps,
};
const service = new UrlService(deps);
diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts
index fc970e2c7a490..2d33f701df595 100644
--- a/src/plugins/share/common/url_service/locators/locator.ts
+++ b/src/plugins/share/common/url_service/locators/locator.ts
@@ -67,13 +67,15 @@ export class Locator implements LocatorPublic
{
state: P,
references: SavedObjectReference[]
): P => {
- return this.definition.inject ? this.definition.inject(state, references) : state;
+ if (!this.definition.inject) return state;
+ return this.definition.inject(state, references);
};
public readonly extract: PersistableState
['extract'] = (
state: P
): { state: P; references: SavedObjectReference[] } => {
- return this.definition.extract ? this.definition.extract(state) : { state, references: [] };
+ if (!this.definition.extract) return { state, references: [] };
+ return this.definition.extract(state);
};
// LocatorPublic
----------------------------------------------------------
diff --git a/src/plugins/share/common/url_service/locators/locator_client.ts b/src/plugins/share/common/url_service/locators/locator_client.ts
index 587083551aa6d..7dd69165be5dd 100644
--- a/src/plugins/share/common/url_service/locators/locator_client.ts
+++ b/src/plugins/share/common/url_service/locators/locator_client.ts
@@ -7,9 +7,12 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
+import { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common';
+import { SavedObjectReference } from 'kibana/server';
import type { LocatorDependencies } from './locator';
-import type { LocatorDefinition, LocatorPublic, ILocatorClient } from './types';
+import type { LocatorDefinition, LocatorPublic, ILocatorClient, LocatorData } from './types';
import { Locator } from './locator';
+import { LocatorMigrationFunction, LocatorsMigrationMap } from '.';
export type LocatorClientDependencies = LocatorDependencies;
@@ -44,4 +47,91 @@ export class LocatorClient implements ILocatorClient {
public get
(id: string): undefined | LocatorPublic
{
return this.locators.get(id);
}
+
+ protected getOrThrow
(id: string): LocatorPublic
{
+ const locator = this.locators.get(id);
+ if (!locator) throw new Error(`Locator [ID = "${id}"] is not registered.`);
+ return locator;
+ }
+
+ public migrations(): { [locatorId: string]: MigrateFunctionsObject } {
+ const migrations: { [locatorId: string]: MigrateFunctionsObject } = {};
+
+ for (const locator of this.locators.values()) {
+ migrations[locator.id] = locator.migrations;
+ }
+
+ return migrations;
+ }
+
+ // PersistableStateService ----------------------------------------------------------
+
+ public telemetry(
+ state: LocatorData,
+ collector: Record
+ ): Record {
+ for (const locator of this.locators.values()) {
+ collector = locator.telemetry(state.state, collector);
+ }
+
+ return collector;
+ }
+
+ public inject(state: LocatorData, references: SavedObjectReference[]): LocatorData {
+ const locator = this.getOrThrow(state.id);
+ const filteredReferences = references
+ .filter((ref) => ref.name.startsWith('params:'))
+ .map((ref) => ({
+ ...ref,
+ name: ref.name.substr('params:'.length),
+ }));
+ return {
+ ...state,
+ state: locator.inject(state.state, filteredReferences),
+ };
+ }
+
+ public extract(state: LocatorData): { state: LocatorData; references: SavedObjectReference[] } {
+ const locator = this.getOrThrow(state.id);
+ const extracted = locator.extract(state.state);
+ return {
+ state: {
+ ...state,
+ state: extracted.state,
+ },
+ references: extracted.references.map((ref) => ({
+ ...ref,
+ name: 'params:' + ref.name,
+ })),
+ };
+ }
+
+ public readonly getAllMigrations = (): LocatorsMigrationMap => {
+ const locatorParamsMigrations = this.migrations();
+ const locatorMigrations: LocatorsMigrationMap = {};
+ const versions = new Set();
+
+ for (const migrationMap of Object.values(locatorParamsMigrations))
+ for (const version of Object.keys(migrationMap)) versions.add(version);
+
+ for (const version of versions.values()) {
+ const migration: LocatorMigrationFunction = (locator) => {
+ const locatorMigrationsMap = locatorParamsMigrations[locator.id];
+ if (!locatorMigrationsMap) return locator;
+
+ const migrationFunction = locatorMigrationsMap[version];
+ if (!migrationFunction) return locator;
+
+ return {
+ ...locator,
+ version,
+ state: migrationFunction(locator.state),
+ };
+ };
+
+ locatorMigrations[version] = migration;
+ }
+
+ return locatorMigrations;
+ };
}
diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts
index ab0efa9b2375a..c64dc588aaf22 100644
--- a/src/plugins/share/common/url_service/locators/types.ts
+++ b/src/plugins/share/common/url_service/locators/types.ts
@@ -8,13 +8,18 @@
import type { SerializableRecord } from '@kbn/utility-types';
import { DependencyList } from 'react';
-import { PersistableState } from 'src/plugins/kibana_utils/common';
+import {
+ MigrateFunction,
+ PersistableState,
+ PersistableStateService,
+ VersionedState,
+} from 'src/plugins/kibana_utils/common';
import type { FormatSearchParamsOptions } from './redirect';
/**
* URL locator registry.
*/
-export interface ILocatorClient {
+export interface ILocatorClient extends PersistableStateService {
/**
* Create and register a new locator.
*
@@ -141,3 +146,22 @@ export interface KibanaLocation {
*/
state: S;
}
+
+/**
+ * Represents a serializable state of a locator. Includes locator ID, version
+ * and its params.
+ */
+export interface LocatorData
+ extends VersionedState,
+ SerializableRecord {
+ /**
+ * Locator ID.
+ */
+ id: string;
+}
+
+export interface LocatorsMigrationMap {
+ [semver: string]: LocatorMigrationFunction;
+}
+
+export type LocatorMigrationFunction = MigrateFunction;
diff --git a/src/plugins/share/common/url_service/mocks.ts b/src/plugins/share/common/url_service/mocks.ts
index dd86e2398589e..24ba226818427 100644
--- a/src/plugins/share/common/url_service/mocks.ts
+++ b/src/plugins/share/common/url_service/mocks.ts
@@ -18,7 +18,7 @@ export class MockUrlService extends UrlService {
getUrl: async ({ app, path }, { absolute }) => {
return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`;
},
- shortUrls: {
+ shortUrls: () => ({
get: () => ({
create: async () => {
throw new Error('Not implemented.');
@@ -33,7 +33,7 @@ export class MockUrlService extends UrlService {
throw new Error('Not implemented.');
},
}),
- },
+ }),
});
}
}
diff --git a/src/plugins/share/common/url_service/short_urls/types.ts b/src/plugins/share/common/url_service/short_urls/types.ts
index db744a25f9f79..698ffe7b8421b 100644
--- a/src/plugins/share/common/url_service/short_urls/types.ts
+++ b/src/plugins/share/common/url_service/short_urls/types.ts
@@ -6,9 +6,8 @@
* Side Public License, v 1.
*/
-import { SerializableRecord } from '@kbn/utility-types';
-import { VersionedState } from 'src/plugins/kibana_utils/common';
-import { LocatorPublic } from '../locators';
+import type { SerializableRecord } from '@kbn/utility-types';
+import type { LocatorPublic, ILocatorClient, LocatorData } from '../locators';
/**
* A factory for Short URL Service. We need this factory as the dependency
@@ -21,6 +20,10 @@ export interface IShortUrlClientFactory {
get(dependencies: D): IShortUrlClient;
}
+export type IShortUrlClientFactoryProvider = (params: {
+ locators: ILocatorClient;
+}) => IShortUrlClientFactory;
+
/**
* CRUD-like API for short URLs.
*/
@@ -128,14 +131,4 @@ export interface ShortUrlData;
}
-/**
- * Represents a serializable state of a locator. Includes locator ID, version
- * and its params.
- */
-export interface LocatorData
- extends VersionedState {
- /**
- * Locator ID.
- */
- id: string;
-}
+export type { LocatorData };
diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts
index dedb81720865d..24e2ea0b62379 100644
--- a/src/plugins/share/common/url_service/url_service.ts
+++ b/src/plugins/share/common/url_service/url_service.ts
@@ -7,10 +7,10 @@
*/
import { LocatorClient, LocatorClientDependencies } from './locators';
-import { IShortUrlClientFactory } from './short_urls';
+import { IShortUrlClientFactoryProvider, IShortUrlClientFactory } from './short_urls';
export interface UrlServiceDependencies extends LocatorClientDependencies {
- shortUrls: IShortUrlClientFactory;
+ shortUrls: IShortUrlClientFactoryProvider;
}
/**
@@ -26,6 +26,8 @@ export class UrlService {
constructor(protected readonly deps: UrlServiceDependencies) {
this.locators = new LocatorClient(deps);
- this.shortUrls = deps.shortUrls;
+ this.shortUrls = deps.shortUrls({
+ locators: this.locators,
+ });
}
}
diff --git a/src/plugins/share/public/mocks.ts b/src/plugins/share/public/mocks.ts
index 73df7257290f0..33cdf141de9f3 100644
--- a/src/plugins/share/public/mocks.ts
+++ b/src/plugins/share/public/mocks.ts
@@ -18,7 +18,7 @@ const url = new UrlService({
getUrl: async ({ app, path }, { absolute }) => {
return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`;
},
- shortUrls: {
+ shortUrls: () => ({
get: () => ({
create: async () => {
throw new Error('Not implemented');
@@ -33,7 +33,7 @@ const url = new UrlService({
throw new Error('Not implemented.');
},
}),
- },
+ }),
});
const createSetupContract = (): Setup => {
diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts
index 103fbb50bb95f..fd8a5fd7541a6 100644
--- a/src/plugins/share/public/plugin.ts
+++ b/src/plugins/share/public/plugin.ts
@@ -104,7 +104,7 @@ export class SharePlugin implements Plugin {
});
return url;
},
- shortUrls: {
+ shortUrls: () => ({
get: () => ({
create: async () => {
throw new Error('Not implemented');
@@ -119,7 +119,7 @@ export class SharePlugin implements Plugin {
throw new Error('Not implemented.');
},
}),
- },
+ }),
});
this.url.locators.create(new LegacyShortUrlLocatorDefinition());
diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts
index f0e4abf9eb589..d79588420fe87 100644
--- a/src/plugins/share/server/plugin.ts
+++ b/src/plugins/share/server/plugin.ts
@@ -9,11 +9,14 @@
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server';
-import { url } from './saved_objects';
import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../common/constants';
import { UrlService } from '../common/url_service';
-import { ServerUrlService, ServerShortUrlClientFactory } from './url_service';
-import { registerUrlServiceRoutes } from './url_service/http/register_url_service_routes';
+import {
+ ServerUrlService,
+ ServerShortUrlClientFactory,
+ registerUrlServiceRoutes,
+ registerUrlServiceSavedObjectType,
+} from './url_service';
import { LegacyShortUrlLocatorDefinition } from '../common/url_service/locators/legacy_short_url_locator';
/** @public */
@@ -44,18 +47,17 @@ export class SharePlugin implements Plugin {
getUrl: async () => {
throw new Error('Locator .getUrl() currently is not supported on the server.');
},
- shortUrls: new ServerShortUrlClientFactory({
- currentVersion: this.version,
- }),
+ shortUrls: ({ locators }) =>
+ new ServerShortUrlClientFactory({
+ currentVersion: this.version,
+ locators,
+ }),
});
-
this.url.locators.create(new LegacyShortUrlLocatorDefinition());
- const router = core.http.createRouter();
-
- registerUrlServiceRoutes(core, router, this.url);
+ registerUrlServiceSavedObjectType(core.savedObjects, this.url);
+ registerUrlServiceRoutes(core, core.http.createRouter(), this.url);
- core.savedObjects.registerType(url);
core.uiSettings.register({
[CSV_SEPARATOR_SETTING]: {
name: i18n.translate('share.advancedSettings.csv.separatorTitle', {
diff --git a/src/plugins/share/server/saved_objects/url.ts b/src/plugins/share/server/saved_objects/url.ts
deleted file mode 100644
index 6288e87f629f5..0000000000000
--- a/src/plugins/share/server/saved_objects/url.ts
+++ /dev/null
@@ -1,67 +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 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 { SavedObjectsType } from 'kibana/server';
-
-export const url: SavedObjectsType = {
- name: 'url',
- namespaceType: 'single',
- hidden: false,
- management: {
- icon: 'link',
- defaultSearchField: 'url',
- importableAndExportable: true,
- getTitle(obj) {
- return `/goto/${encodeURIComponent(obj.id)}`;
- },
- getInAppUrl(obj) {
- return {
- path: '/goto/' + encodeURIComponent(obj.id),
- uiCapabilitiesPath: '',
- };
- },
- },
- mappings: {
- properties: {
- slug: {
- type: 'text',
- fields: {
- keyword: {
- type: 'keyword',
- },
- },
- },
- accessCount: {
- type: 'long',
- },
- accessDate: {
- type: 'date',
- },
- createDate: {
- type: 'date',
- },
- // Legacy field - contains already pre-formatted final URL.
- // This is here to support old saved objects that have this field.
- // TODO: Remove this field and execute a migration to the new format.
- url: {
- type: 'text',
- fields: {
- keyword: {
- type: 'keyword',
- ignore_above: 2048,
- },
- },
- },
- // Information needed to load and execute a locator.
- locatorJSON: {
- type: 'text',
- index: false,
- },
- },
- },
-};
diff --git a/src/plugins/share/server/url_service/index.ts b/src/plugins/share/server/url_service/index.ts
index 068a5289d42ed..62d1329371736 100644
--- a/src/plugins/share/server/url_service/index.ts
+++ b/src/plugins/share/server/url_service/index.ts
@@ -8,3 +8,5 @@
export * from './types';
export * from './short_urls';
+export { registerUrlServiceRoutes } from './http/register_url_service_routes';
+export { registerUrlServiceSavedObjectType } from './saved_objects/register_url_service_saved_object_type';
diff --git a/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.test.ts b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.test.ts
new file mode 100644
index 0000000000000..651169f6101a9
--- /dev/null
+++ b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.test.ts
@@ -0,0 +1,144 @@
+/*
+ * 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 { SerializableRecord } from '@kbn/utility-types';
+import type {
+ SavedObjectMigrationMap,
+ SavedObjectsType,
+ SavedObjectUnsanitizedDoc,
+} from 'kibana/server';
+import { ServerShortUrlClientFactory } from '..';
+import { UrlService, LocatorDefinition } from '../../../common/url_service';
+import { LegacyShortUrlLocatorDefinition } from '../../../common/url_service/locators/legacy_short_url_locator';
+import { MemoryShortUrlStorage } from '../short_urls/storage/memory_short_url_storage';
+import { ShortUrlSavedObjectAttributes } from '../short_urls/storage/saved_object_short_url_storage';
+import { registerUrlServiceSavedObjectType } from './register_url_service_saved_object_type';
+
+const setup = () => {
+ const currentVersion = '7.7.7';
+ const service = new UrlService({
+ getUrl: () => {
+ throw new Error('Not implemented.');
+ },
+ navigate: () => {
+ throw new Error('Not implemented.');
+ },
+ shortUrls: ({ locators }) =>
+ new ServerShortUrlClientFactory({
+ currentVersion,
+ locators,
+ }),
+ });
+ const definition = new LegacyShortUrlLocatorDefinition();
+ const locator = service.locators.create(definition);
+ const storage = new MemoryShortUrlStorage();
+ const client = service.shortUrls.get({ storage });
+
+ let type: SavedObjectsType;
+ registerUrlServiceSavedObjectType(
+ {
+ registerType: (urlSavedObjectType) => {
+ type = urlSavedObjectType;
+ },
+ },
+ service
+ );
+
+ return {
+ type: type!,
+ client,
+ service,
+ storage,
+ locator,
+ definition,
+ currentVersion,
+ };
+};
+
+describe('migrations', () => {
+ test('returns empty migrations object if there are no migrations', () => {
+ const { type } = setup();
+
+ expect((type.migrations as () => SavedObjectMigrationMap)()).toEqual({});
+ });
+
+ test('migrates locator to the latest version', () => {
+ interface FooLocatorParamsOld extends SerializableRecord {
+ color: string;
+ indexPattern: string;
+ }
+
+ interface FooLocatorParams extends SerializableRecord {
+ color: string;
+ indexPatterns: string[];
+ }
+
+ class FooLocatorDefinition implements LocatorDefinition {
+ public readonly id = 'FOO_LOCATOR';
+
+ public async getLocation() {
+ return {
+ app: 'foo',
+ path: '',
+ state: {},
+ };
+ }
+
+ migrations = {
+ '8.0.0': ({ indexPattern, ...rest }: FooLocatorParamsOld): FooLocatorParams => ({
+ ...rest,
+ indexPatterns: [indexPattern],
+ }),
+ };
+ }
+
+ const { type, service } = setup();
+
+ service.locators.create(new FooLocatorDefinition());
+
+ const migrationFunction = (type.migrations as () => SavedObjectMigrationMap)()['8.0.0'];
+
+ expect(typeof migrationFunction).toBe('function');
+
+ const doc1: SavedObjectUnsanitizedDoc = {
+ id: 'foo',
+ attributes: {
+ accessCount: 0,
+ accessDate: 0,
+ createDate: 0,
+ locatorJSON: JSON.stringify({
+ id: 'FOO_LOCATOR',
+ version: '7.7.7',
+ state: {
+ color: 'red',
+ indexPattern: 'myIndex',
+ },
+ }),
+ url: '',
+ },
+ type: 'url',
+ };
+
+ const doc2 = migrationFunction(doc1, {} as any);
+
+ expect(doc2.id).toBe('foo');
+ expect(doc2.type).toBe('url');
+ expect(doc2.attributes.accessCount).toBe(0);
+ expect(doc2.attributes.accessDate).toBe(0);
+ expect(doc2.attributes.createDate).toBe(0);
+ expect(doc2.attributes.url).toBe('');
+ expect(JSON.parse(doc2.attributes.locatorJSON)).toEqual({
+ id: 'FOO_LOCATOR',
+ version: '8.0.0',
+ state: {
+ color: 'red',
+ indexPatterns: ['myIndex'],
+ },
+ });
+ });
+});
diff --git a/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.ts b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.ts
new file mode 100644
index 0000000000000..b2fcefcc767cf
--- /dev/null
+++ b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 {
+ SavedObjectMigrationMap,
+ SavedObjectsServiceSetup,
+ SavedObjectsType,
+} from 'kibana/server';
+import type { LocatorData } from 'src/plugins/share/common/url_service';
+import type { ServerUrlService } from '..';
+
+export const registerUrlServiceSavedObjectType = (
+ so: Pick,
+ service: ServerUrlService
+) => {
+ const urlSavedObjectType: SavedObjectsType = {
+ name: 'url',
+ namespaceType: 'single',
+ hidden: false,
+ management: {
+ icon: 'link',
+ defaultSearchField: 'url',
+ importableAndExportable: true,
+ getTitle(obj) {
+ return `/goto/${encodeURIComponent(obj.id)}`;
+ },
+ getInAppUrl(obj) {
+ return {
+ path: '/goto/' + encodeURIComponent(obj.id),
+ uiCapabilitiesPath: '',
+ };
+ },
+ },
+ mappings: {
+ properties: {
+ slug: {
+ type: 'text',
+ fields: {
+ keyword: {
+ type: 'keyword',
+ },
+ },
+ },
+ accessCount: {
+ type: 'long',
+ },
+ accessDate: {
+ type: 'date',
+ },
+ createDate: {
+ type: 'date',
+ },
+ // Legacy field - contains already pre-formatted final URL.
+ // This is here to support old saved objects that have this field.
+ // TODO: Remove this field and execute a migration to the new format.
+ url: {
+ type: 'text',
+ fields: {
+ keyword: {
+ type: 'keyword',
+ ignore_above: 2048,
+ },
+ },
+ },
+ // Information needed to load and execute a locator.
+ locatorJSON: {
+ type: 'text',
+ index: false,
+ },
+ },
+ },
+ migrations: () => {
+ const locatorMigrations = service.locators.getAllMigrations();
+ const savedObjectLocatorMigrations: SavedObjectMigrationMap = {};
+
+ for (const [version, locatorMigration] of Object.entries(locatorMigrations)) {
+ savedObjectLocatorMigrations[version] = (doc) => {
+ const locator = JSON.parse(doc.attributes.locatorJSON) as LocatorData;
+ doc.attributes = {
+ ...doc.attributes,
+ locatorJSON: JSON.stringify(locatorMigration(locator)),
+ };
+ return doc;
+ };
+ }
+
+ return savedObjectLocatorMigrations;
+ },
+ };
+
+ so.registerType(urlSavedObjectType);
+};
diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts
index ac684eb03a9d5..503748a2b1cad 100644
--- a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts
+++ b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts
@@ -7,9 +7,11 @@
*/
import { ServerShortUrlClientFactory } from './short_url_client_factory';
-import { UrlService } from '../../../common/url_service';
+import { UrlService, LocatorDefinition } from '../../../common/url_service';
import { LegacyShortUrlLocatorDefinition } from '../../../common/url_service/locators/legacy_short_url_locator';
import { MemoryShortUrlStorage } from './storage/memory_short_url_storage';
+import { SerializableRecord } from '@kbn/utility-types';
+import { SavedObjectReference } from 'kibana/server';
const setup = () => {
const currentVersion = '1.2.3';
@@ -20,9 +22,11 @@ const setup = () => {
navigate: () => {
throw new Error('Not implemented.');
},
- shortUrls: new ServerShortUrlClientFactory({
- currentVersion,
- }),
+ shortUrls: ({ locators }) =>
+ new ServerShortUrlClientFactory({
+ currentVersion,
+ locators,
+ }),
});
const definition = new LegacyShortUrlLocatorDefinition();
const locator = service.locators.create(definition);
@@ -177,4 +181,111 @@ describe('ServerShortUrlClient', () => {
);
});
});
+
+ describe('Persistable State', () => {
+ interface FooLocatorParams extends SerializableRecord {
+ dashboardId: string;
+ indexPatternId: string;
+ }
+
+ class FooLocatorDefinition implements LocatorDefinition {
+ public readonly id = 'FOO_LOCATOR';
+
+ public readonly getLocation = async () => ({
+ app: 'foo_app',
+ path: '/foo/path',
+ state: {},
+ });
+
+ public readonly extract = (
+ state: FooLocatorParams
+ ): { state: FooLocatorParams; references: SavedObjectReference[] } => ({
+ state,
+ references: [
+ {
+ id: state.dashboardId,
+ type: 'dashboard',
+ name: 'dashboardId',
+ },
+ {
+ id: state.indexPatternId,
+ type: 'index_pattern',
+ name: 'indexPatternId',
+ },
+ ],
+ });
+
+ public readonly inject = (
+ state: FooLocatorParams,
+ references: SavedObjectReference[]
+ ): FooLocatorParams => {
+ const dashboard = references.find(
+ (ref) => ref.type === 'dashboard' && ref.name === 'dashboardId'
+ );
+ const indexPattern = references.find(
+ (ref) => ref.type === 'index_pattern' && ref.name === 'indexPatternId'
+ );
+
+ return {
+ ...state,
+ dashboardId: dashboard ? dashboard.id : '',
+ indexPatternId: indexPattern ? indexPattern.id : '',
+ };
+ };
+ }
+
+ test('extracts and persists references', async () => {
+ const { service, client, storage } = setup();
+ const locator = service.locators.create(new FooLocatorDefinition());
+ const shortUrl = await client.create({
+ locator,
+ params: {
+ dashboardId: '123',
+ indexPatternId: '456',
+ },
+ });
+ const record = await storage.getById(shortUrl.data.id);
+
+ expect(record.references).toEqual([
+ {
+ id: '123',
+ type: 'dashboard',
+ name: 'locator:params:dashboardId',
+ },
+ {
+ id: '456',
+ type: 'index_pattern',
+ name: 'locator:params:indexPatternId',
+ },
+ ]);
+ });
+
+ test('injects references', async () => {
+ const { service, client, storage } = setup();
+ const locator = service.locators.create(new FooLocatorDefinition());
+ const shortUrl1 = await client.create({
+ locator,
+ params: {
+ dashboardId: '3',
+ indexPatternId: '5',
+ },
+ });
+ const record1 = await storage.getById(shortUrl1.data.id);
+
+ record1.data.locator.state = {};
+
+ await storage.update(record1.data.id, record1.data);
+
+ const record2 = await storage.getById(shortUrl1.data.id);
+
+ expect(record2.data.locator.state).toEqual({});
+
+ const shortUrl2 = await client.get(shortUrl1.data.id);
+
+ expect(shortUrl2.data.locator.state).toEqual({
+ dashboardId: '3',
+ indexPatternId: '5',
+ });
+ });
+ });
});
diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.ts
index caaa76bef172d..1efece073d955 100644
--- a/src/plugins/share/server/url_service/short_urls/short_url_client.ts
+++ b/src/plugins/share/server/url_service/short_urls/short_url_client.ts
@@ -7,8 +7,17 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
+import { SavedObjectReference } from 'kibana/server';
import { generateSlug } from 'random-word-slugs';
-import type { IShortUrlClient, ShortUrl, ShortUrlCreateParams } from '../../../common/url_service';
+import { ShortUrlRecord } from '.';
+import type {
+ IShortUrlClient,
+ ShortUrl,
+ ShortUrlCreateParams,
+ ILocatorClient,
+ ShortUrlData,
+ LocatorData,
+} from '../../../common/url_service';
import type { ShortUrlStorage } from './types';
import { validateSlug } from './util';
@@ -36,6 +45,11 @@ export interface ServerShortUrlClientDependencies {
* Storage provider for short URLs.
*/
storage: ShortUrlStorage;
+
+ /**
+ * The locators service.
+ */
+ locators: ILocatorClient;
}
export class ServerShortUrlClient implements IShortUrlClient {
@@ -64,44 +78,80 @@ export class ServerShortUrlClient implements IShortUrlClient {
}
}
+ const extracted = this.extractReferences({
+ id: locator.id,
+ version: currentVersion,
+ state: params,
+ });
const now = Date.now();
- const data = await storage.create({
- accessCount: 0,
- accessDate: now,
- createDate: now,
- slug,
- locator: {
- id: locator.id,
- version: currentVersion,
- state: params,
+
+ const data = await storage.create(
+ {
+ accessCount: 0,
+ accessDate: now,
+ createDate: now,
+ slug,
+ locator: extracted.state as LocatorData
,
},
- });
+ { references: extracted.references }
+ );
return {
data,
};
}
- public async get(id: string): Promise {
- const { storage } = this.dependencies;
- const data = await storage.getById(id);
+ private extractReferences(locatorData: LocatorData): {
+ state: LocatorData;
+ references: SavedObjectReference[];
+ } {
+ const { locators } = this.dependencies;
+ const { state, references } = locators.extract(locatorData);
+ return {
+ state,
+ references: references.map((ref) => ({
+ ...ref,
+ name: 'locator:' + ref.name,
+ })),
+ };
+ }
+ private injectReferences({ data, references }: ShortUrlRecord): ShortUrlData {
+ const { locators } = this.dependencies;
+ const locatorReferences = references
+ .filter((ref) => ref.name.startsWith('locator:'))
+ .map((ref) => ({
+ ...ref,
+ name: ref.name.substr('locator:'.length),
+ }));
return {
- data,
+ ...data,
+ locator: locators.inject(data.locator, locatorReferences),
};
}
- public async delete(id: string): Promise {
+ public async get(id: string): Promise {
const { storage } = this.dependencies;
- await storage.delete(id);
+ const record = await storage.getById(id);
+ const data = this.injectReferences(record);
+
+ return {
+ data,
+ };
}
public async resolve(slug: string): Promise {
const { storage } = this.dependencies;
- const data = await storage.getBySlug(slug);
+ const record = await storage.getBySlug(slug);
+ const data = this.injectReferences(record);
return {
data,
};
}
+
+ public async delete(id: string): Promise {
+ const { storage } = this.dependencies;
+ await storage.delete(id);
+ }
}
diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts b/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts
index 696233b7a1ca5..63456c36daa68 100644
--- a/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts
+++ b/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts
@@ -8,7 +8,7 @@
import { SavedObjectsClientContract } from 'kibana/server';
import { ShortUrlStorage } from './types';
-import type { IShortUrlClientFactory } from '../../../common/url_service';
+import type { IShortUrlClientFactory, ILocatorClient } from '../../../common/url_service';
import { ServerShortUrlClient } from './short_url_client';
import { SavedObjectShortUrlStorage } from './storage/saved_object_short_url_storage';
@@ -20,6 +20,11 @@ export interface ServerShortUrlClientFactoryDependencies {
* Current version of Kibana, e.g. 7.15.0.
*/
currentVersion: string;
+
+ /**
+ * Locators service.
+ */
+ locators: ILocatorClient;
}
export interface ServerShortUrlClientFactoryCreateParams {
@@ -39,9 +44,11 @@ export class ServerShortUrlClientFactory
savedObjects: params.savedObjects!,
savedObjectType: 'url',
});
+ const { currentVersion, locators } = this.dependencies;
const client = new ServerShortUrlClient({
storage,
- currentVersion: this.dependencies.currentVersion,
+ currentVersion,
+ locators,
});
return client;
diff --git a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts
index d178e0b81786c..5d1b0bfa0bf55 100644
--- a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts
+++ b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts
@@ -41,6 +41,46 @@ describe('.create()', () => {
});
});
+describe('.update()', () => {
+ test('can update an existing short URL', async () => {
+ const storage = new MemoryShortUrlStorage();
+ const now = Date.now();
+ const url1 = await storage.create({
+ accessCount: 0,
+ createDate: now,
+ accessDate: now,
+ locator: {
+ id: 'TEST_LOCATOR',
+ version: '7.11',
+ state: {
+ foo: 'bar',
+ },
+ },
+ slug: 'test-slug',
+ });
+
+ await storage.update(url1.id, {
+ accessCount: 1,
+ });
+
+ const url2 = await storage.getById(url1.id);
+
+ expect(url1.accessCount).toBe(0);
+ expect(url2.data.accessCount).toBe(1);
+ });
+
+ test('throws when URL does not exist', async () => {
+ const storage = new MemoryShortUrlStorage();
+ const [, error] = await of(
+ storage.update('DOES_NOT_EXIST', {
+ accessCount: 1,
+ })
+ );
+
+ expect(error).toBeInstanceOf(Error);
+ });
+});
+
describe('.getById()', () => {
test('can fetch by ID a newly created short URL', async () => {
const storage = new MemoryShortUrlStorage();
@@ -58,7 +98,7 @@ describe('.getById()', () => {
},
slug: 'test-slug',
});
- const url2 = await storage.getById(url1.id);
+ const url2 = (await storage.getById(url1.id)).data;
expect(url2.accessCount).toBe(0);
expect(url1.createDate).toBe(now);
@@ -112,7 +152,7 @@ describe('.getBySlug()', () => {
},
slug: 'test-slug',
});
- const url2 = await storage.getBySlug('test-slug');
+ const url2 = (await storage.getBySlug('test-slug')).data;
expect(url2.accessCount).toBe(0);
expect(url1.createDate).toBe(now);
diff --git a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts
index 40d76a91154ba..fafd00344eecd 100644
--- a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts
+++ b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts
@@ -9,35 +9,54 @@
import { v4 as uuidv4 } from 'uuid';
import type { SerializableRecord } from '@kbn/utility-types';
import { ShortUrlData } from 'src/plugins/share/common/url_service/short_urls/types';
-import { ShortUrlStorage } from '../types';
+import { SavedObjectReference } from 'kibana/server';
+import { ShortUrlStorage, ShortUrlRecord } from '../types';
+
+const clone = (obj: P): P => JSON.parse(JSON.stringify(obj)) as P;
export class MemoryShortUrlStorage implements ShortUrlStorage {
- private urls = new Map();
+ private urls = new Map();
public async create(
- data: Omit, 'id'>
+ data: Omit, 'id'>,
+ { references = [] }: { references?: SavedObjectReference[] } = {}
): Promise> {
const id = uuidv4();
- const url: ShortUrlData = { ...data, id };
+ const url: ShortUrlRecord
= {
+ data: { ...data, id },
+ references,
+ };
this.urls.set(id, url);
- return url;
+
+ return clone(url.data);
+ }
+
+ public async update
(
+ id: string,
+ data: Partial, 'id'>>,
+ { references }: { references?: SavedObjectReference[] } = {}
+ ): Promise {
+ const so = await this.getById(id);
+ Object.assign(so.data, data);
+ if (references) so.references = references;
+ this.urls.set(id, so);
}
public async getById(
id: string
- ): Promise> {
+ ): Promise> {
if (!this.urls.has(id)) {
throw new Error(`No short url with id "${id}"`);
}
- return this.urls.get(id)! as ShortUrlData;
+ return clone(this.urls.get(id)! as ShortUrlRecord
);
}
public async getBySlug
(
slug: string
- ): Promise> {
+ ): Promise> {
for (const url of this.urls.values()) {
- if (url.slug === slug) {
- return url as ShortUrlData;
+ if (url.data.slug === slug) {
+ return clone(url as ShortUrlRecord
);
}
}
throw new Error(`No short url with slug "${slug}".`);
@@ -45,7 +64,7 @@ export class MemoryShortUrlStorage implements ShortUrlStorage {
public async exists(slug: string): Promise {
for (const url of this.urls.values()) {
- if (url.slug === slug) {
+ if (url.data.slug === slug) {
return true;
}
}
diff --git a/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts b/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts
index c66db6d82cdbd..792dfabde3cab 100644
--- a/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts
+++ b/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts
@@ -7,7 +7,8 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
-import { SavedObject, SavedObjectsClientContract } from 'kibana/server';
+import { SavedObject, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server';
+import { ShortUrlRecord } from '..';
import { LEGACY_SHORT_URL_LOCATOR_ID } from '../../../../common/url_service/locators/legacy_short_url_locator';
import { ShortUrlData } from '../../../../common/url_service/short_urls/types';
import { ShortUrlStorage } from '../types';
@@ -85,12 +86,15 @@ const createShortUrlData = (
};
const createAttributes =
(
- data: Omit, 'id'>
+ data: Partial, 'id'>>
): ShortUrlSavedObjectAttributes => {
- const { locator, ...rest } = data;
+ const { accessCount = 0, accessDate = 0, createDate = 0, slug = '', locator } = data;
const attributes: ShortUrlSavedObjectAttributes = {
- ...rest,
- locatorJSON: JSON.stringify(locator),
+ accessCount,
+ accessDate,
+ createDate,
+ slug,
+ locatorJSON: locator ? JSON.stringify(locator) : '',
url: '',
};
@@ -106,30 +110,49 @@ export class SavedObjectShortUrlStorage implements ShortUrlStorage {
constructor(private readonly dependencies: SavedObjectShortUrlStorageDependencies) {}
public async create(
- data: Omit, 'id'>
+ data: Omit, 'id'>,
+ { references }: { references?: SavedObjectReference[] } = {}
): Promise> {
const { savedObjects, savedObjectType } = this.dependencies;
const attributes = createAttributes(data);
const savedObject = await savedObjects.create(savedObjectType, attributes, {
refresh: true,
+ references,
});
return createShortUrlData(savedObject);
}
+ public async update
(
+ id: string,
+ data: Partial, 'id'>>,
+ { references }: { references?: SavedObjectReference[] } = {}
+ ): Promise {
+ const { savedObjects, savedObjectType } = this.dependencies;
+ const attributes = createAttributes(data);
+
+ await savedObjects.update(savedObjectType, id, attributes, {
+ refresh: true,
+ references,
+ });
+ }
+
public async getById(
id: string
- ): Promise> {
+ ): Promise> {
const { savedObjects, savedObjectType } = this.dependencies;
const savedObject = await savedObjects.get(savedObjectType, id);
- return createShortUrlData(savedObject);
+ return {
+ data: createShortUrlData
(savedObject),
+ references: savedObject.references,
+ };
}
public async getBySlug
(
slug: string
- ): Promise> {
+ ): Promise> {
const { savedObjects } = this.dependencies;
const search = `(attributes.slug:"${escapeSearchReservedChars(slug)}")`;
const result = await savedObjects.find({
@@ -143,7 +166,10 @@ export class SavedObjectShortUrlStorage implements ShortUrlStorage {
const savedObject = result.saved_objects[0] as ShortUrlSavedObject;
- return createShortUrlData(savedObject);
+ return {
+ data: createShortUrlData
(savedObject),
+ references: savedObject.references,
+ };
}
public async exists(slug: string): Promise {
diff --git a/src/plugins/share/server/url_service/short_urls/types.ts b/src/plugins/share/server/url_service/short_urls/types.ts
index 7aab70ca49519..9a9d9006eb371 100644
--- a/src/plugins/share/server/url_service/short_urls/types.ts
+++ b/src/plugins/share/server/url_service/short_urls/types.ts
@@ -7,6 +7,7 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
+import { SavedObjectReference } from 'kibana/server';
import { ShortUrlData } from '../../../common/url_service/short_urls/types';
/**
@@ -17,20 +18,32 @@ export interface ShortUrlStorage {
* Create and store a new short URL entry.
*/
create(
- data: Omit, 'id'>
+ data: Omit, 'id'>,
+ options?: { references?: SavedObjectReference[] }
): Promise>;
+ /**
+ * Update an existing short URL entry.
+ */
+ update(
+ id: string,
+ data: Partial, 'id'>>,
+ options?: { references?: SavedObjectReference[] }
+ ): Promise;
+
/**
* Fetch a short URL entry by ID.
*/
- getById(id: string): Promise>;
+ getById(
+ id: string
+ ): Promise>;
/**
* Fetch a short URL entry by slug.
*/
getBySlug(
slug: string
- ): Promise>;
+ ): Promise>;
/**
* Checks if a short URL exists by slug.
@@ -42,3 +55,8 @@ export interface ShortUrlStorage {
*/
delete(id: string): Promise;
}
+
+export interface ShortUrlRecord {
+ data: ShortUrlData;
+ references: SavedObjectReference[];
+}
diff --git a/src/plugins/vis_default_editor/kibana.json b/src/plugins/vis_default_editor/kibana.json
index e85c5713eb82c..efed1eab1e494 100644
--- a/src/plugins/vis_default_editor/kibana.json
+++ b/src/plugins/vis_default_editor/kibana.json
@@ -3,7 +3,7 @@
"version": "kibana",
"ui": true,
"optionalPlugins": ["visualize"],
- "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover"],
"owner": {
"name": "Vis Editors",
"githubTeam": "kibana-vis-editors"
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
index dab982e5a8070..f1eebbbdf2116 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
+++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
@@ -26,7 +26,7 @@ import {
} from 'src/plugins/visualizations/public';
import type { Schema } from 'src/plugins/visualizations/public';
import { TimeRange } from 'src/plugins/data/public';
-import { SavedObject } from 'src/plugins/saved_objects/public';
+import { SavedSearch } from 'src/plugins/discover/public';
import { DefaultEditorNavBar } from './navbar';
import { DefaultEditorControls } from './controls';
import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state';
@@ -42,7 +42,7 @@ interface DefaultEditorSideBarProps {
vis: Vis;
isLinkedSearch: boolean;
eventEmitter: EventEmitter;
- savedSearch?: SavedObject;
+ savedSearch?: SavedSearch;
timeRange: TimeRange;
}
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx
index cab27d53b827d..2740f4ff50b4e 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx
+++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx
@@ -25,18 +25,18 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { Vis } from 'src/plugins/visualizations/public';
-import { SavedObject } from 'src/plugins/saved_objects/public';
+import { SavedSearch, getSavedSearchUrl } from '../../../../discover/public';
import { ApplicationStart } from '../../../../../core/public';
import { useKibana } from '../../../../kibana_react/public';
interface LinkedSearchProps {
- savedSearch: SavedObject;
+ savedSearch: SavedSearch;
eventEmitter: EventEmitter;
}
interface SidebarTitleProps {
isLinkedSearch: boolean;
- savedSearch?: SavedObject;
+ savedSearch?: SavedSearch;
vis: Vis;
eventEmitter: EventEmitter;
}
@@ -55,7 +55,7 @@ export function LinkedSearch({ savedSearch, eventEmitter }: LinkedSearchProps) {
}, [eventEmitter]);
const onClickViewInDiscover = useCallback(() => {
application.navigateToApp('discover', {
- path: `#/view/${savedSearch.id}`,
+ path: getSavedSearchUrl(savedSearch.id),
});
}, [application, savedSearch.id]);
diff --git a/src/plugins/vis_types/pie/public/types/types.ts b/src/plugins/vis_types/pie/public/types/types.ts
index a1f41e80fae28..fb5efb5971805 100644
--- a/src/plugins/vis_types/pie/public/types/types.ts
+++ b/src/plugins/vis_types/pie/public/types/types.ts
@@ -8,7 +8,8 @@
import { Position } from '@elastic/charts';
import { UiCounterMetricType } from '@kbn/analytics';
-import { DatatableColumn, SerializedFieldFormat } from '../../../../expressions/public';
+import { DatatableColumn } from '../../../../expressions/public';
+import type { SerializedFieldFormat } from '../../../../field_formats/common';
import { ExpressionValueVisDimension } from '../../../../visualizations/public';
import { ExpressionValuePieLabels } from '../expression_functions/pie_labels';
import { PaletteOutput, ChartsPluginSetup } from '../../../../charts/public';
diff --git a/src/plugins/vis_types/pie/public/utils/get_layers.ts b/src/plugins/vis_types/pie/public/utils/get_layers.ts
index 6ecef858619b5..c9d8da15b78f6 100644
--- a/src/plugins/vis_types/pie/public/utils/get_layers.ts
+++ b/src/plugins/vis_types/pie/public/utils/get_layers.ts
@@ -133,7 +133,6 @@ export const getLayers = (
syncColors: boolean
): PartitionLayer[] => {
const fillLabel: Partial = {
- textInvertible: true,
valueFont: {
fontWeight: 700,
},
diff --git a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx
index d7b7bb14723d7..e6d2638bedf48 100644
--- a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx
+++ b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx
@@ -64,6 +64,8 @@ const DefaultYAxis = () => (
id="left"
domain={withStaticPadding({
fit: false,
+ min: NaN,
+ max: NaN,
})}
position={Position.Left}
groupId={`${MAIN_GROUP_ID}`}
diff --git a/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts b/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts
index 3c76b95bd05ca..98be5efc55a26 100644
--- a/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts
+++ b/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts
@@ -88,8 +88,8 @@ const adaptYaxisParams = (yaxis: IAxis) => {
tickFormat: y.tickFormatter,
domain: withStaticPadding({
fit: y.min === undefined && y.max === undefined,
- min: y.min,
- max: y.max,
+ min: y.min ?? NaN,
+ max: y.max ?? NaN,
}),
};
};
@@ -118,6 +118,8 @@ export const extractAllYAxis = (series: Series[]) => {
groupId,
domain: withStaticPadding({
fit: false,
+ min: NaN,
+ max: NaN,
}),
id: (yaxis?.position || Position.Left) + index,
position: Position.Left,
diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js
index cfeed174307ac..13c17b8f4c38f 100644
--- a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js
+++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js
@@ -81,6 +81,20 @@ describe(`VegaParser.parseAsync`, () => {
})
)
);
+
+ test(`should return a specific error in case of $schema URL not valid`, async () => {
+ const vp = new VegaParser({
+ $schema: 'https://vega.github.io/schema/vega-lite/v4.jsonanythingtobreakthis',
+ mark: 'circle',
+ encoding: { row: { field: 'a' } },
+ });
+
+ await vp.parseAsync();
+
+ expect(vp.error).toBe(
+ 'The URL for the JSON "$schema" is incorrect. Correct the URL, then click Update.'
+ );
+ });
});
describe(`VegaParser._setDefaultValue`, () => {
diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts
index 9000fed7f6116..bf2a6be25c71a 100644
--- a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts
+++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts
@@ -553,25 +553,37 @@ The URL is an identifier only. Kibana and your browser will never access this UR
* @private
*/
private parseSchema(spec: VegaSpec) {
- const schema = schemaParser(spec.$schema);
- const isVegaLite = schema.library === 'vega-lite';
- const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion;
+ try {
+ const schema = schemaParser(spec.$schema);
+ const isVegaLite = schema.library === 'vega-lite';
+ const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion;
- if (versionCompare(schema.version, libVersion) > 0) {
- this._onWarning(
- i18n.translate('visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage', {
+ if (versionCompare(schema.version, libVersion) > 0) {
+ this._onWarning(
+ i18n.translate(
+ 'visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage',
+ {
+ defaultMessage:
+ 'The input spec uses {schemaLibrary} {schemaVersion}, but current version of {schemaLibrary} is {libraryVersion}.',
+ values: {
+ schemaLibrary: schema.library,
+ schemaVersion: schema.version,
+ libraryVersion: libVersion,
+ },
+ }
+ )
+ );
+ }
+
+ return { isVegaLite, libVersion };
+ } catch (e) {
+ throw Error(
+ i18n.translate('visTypeVega.vegaParser.notValidSchemaForInputSpec', {
defaultMessage:
- 'The input spec uses {schemaLibrary} {schemaVersion}, but current version of {schemaLibrary} is {libraryVersion}.',
- values: {
- schemaLibrary: schema.library,
- schemaVersion: schema.version,
- libraryVersion: libVersion,
- },
+ 'The URL for the JSON "$schema" is incorrect. Correct the URL, then click Update.',
})
);
}
-
- return { isVegaLite, libVersion };
}
/**
diff --git a/src/plugins/vis_types/xy/public/components/xy_settings.tsx b/src/plugins/vis_types/xy/public/components/xy_settings.tsx
index 5e02b65822d6c..74aff7535c2d8 100644
--- a/src/plugins/vis_types/xy/public/components/xy_settings.tsx
+++ b/src/plugins/vis_types/xy/public/components/xy_settings.tsx
@@ -71,7 +71,6 @@ function getValueLabelsStyling() {
return {
displayValue: {
fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE },
- fill: { textInverted: false, textContrast: true },
alignment: { horizontal: HorizontalAlignment.Center, vertical: VerticalAlignment.Middle },
},
};
diff --git a/src/plugins/vis_types/xy/public/config/get_axis.ts b/src/plugins/vis_types/xy/public/config/get_axis.ts
index b5cc96830e46a..09495725296cd 100644
--- a/src/plugins/vis_types/xy/public/config/get_axis.ts
+++ b/src/plugins/vis_types/xy/public/config/get_axis.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { identity, isNil } from 'lodash';
+import { identity } from 'lodash';
import { AxisSpec, TickFormatter, YDomainRange, ScaleType as ECScaleType } from '@elastic/charts';
@@ -171,17 +171,5 @@ function getAxisDomain(
const fit = defaultYExtents;
const padding = boundsMargin || undefined;
- if (!isNil(min) && !isNil(max)) {
- return { fit, padding, min, max };
- }
-
- if (!isNil(min)) {
- return { fit, padding, min };
- }
-
- if (!isNil(max)) {
- return { fit, padding, max };
- }
-
- return { fit, padding };
+ return { fit, padding, min: min ?? NaN, max: max ?? NaN };
}
diff --git a/src/plugins/vis_types/xy/public/utils/domain.ts b/src/plugins/vis_types/xy/public/utils/domain.ts
index fa8dd74e3942a..5b1310863979a 100644
--- a/src/plugins/vis_types/xy/public/utils/domain.ts
+++ b/src/plugins/vis_types/xy/public/utils/domain.ts
@@ -33,6 +33,8 @@ export const getXDomain = (params: Aspect['params']): DomainRange => {
return {
minInterval,
+ min: NaN,
+ max: NaN,
};
};
@@ -74,9 +76,9 @@ export const getAdjustedDomain = (
};
}
- return 'interval' in params
- ? {
- minInterval: params.interval,
- }
- : {};
+ return {
+ minInterval: 'interval' in params ? params.interval : undefined,
+ min: NaN,
+ max: NaN,
+ };
};
diff --git a/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts
index 5fe1b03dd8b93..c14e313b1e7a4 100644
--- a/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts
+++ b/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts
@@ -112,7 +112,10 @@ export const getVisConfig = (): VisConfig => {
mode: AxisMode.Normal,
type: 'linear',
},
- domain: {},
+ domain: {
+ min: NaN,
+ max: NaN,
+ },
integersOnly: false,
},
],
@@ -246,7 +249,10 @@ export const getVisConfigMutipleYaxis = (): VisConfig => {
mode: AxisMode.Normal,
type: 'linear',
},
- domain: {},
+ domain: {
+ min: NaN,
+ max: NaN,
+ },
integersOnly: false,
},
],
@@ -435,7 +441,10 @@ export const getVisConfigPercentiles = (): VisConfig => {
mode: AxisMode.Normal,
type: 'linear',
},
- domain: {},
+ domain: {
+ min: NaN,
+ max: NaN,
+ },
integersOnly: false,
},
],
diff --git a/src/plugins/vis_types/xy/public/vis_component.tsx b/src/plugins/vis_types/xy/public/vis_component.tsx
index f4d566f49602e..515ad3e7eaf6f 100644
--- a/src/plugins/vis_types/xy/public/vis_component.tsx
+++ b/src/plugins/vis_types/xy/public/vis_component.tsx
@@ -19,6 +19,7 @@ import {
ScaleType,
AccessorFn,
Accessor,
+ XYBrushEvent,
} from '@elastic/charts';
import { compact } from 'lodash';
@@ -131,7 +132,10 @@ const VisComponent = (props: VisComponentProps) => {
): BrushEndListener | undefined => {
if (xAccessor !== null && isInterval) {
return (brushArea) => {
- const event = getBrushFromChartBrushEventFn(visData, xAccessor)(brushArea);
+ const event = getBrushFromChartBrushEventFn(
+ visData,
+ xAccessor
+ )(brushArea as XYBrushEvent);
props.fireEvent(event);
};
}
diff --git a/src/plugins/visualizations/common/expression_functions/xy_dimension.ts b/src/plugins/visualizations/common/expression_functions/xy_dimension.ts
index 82538fea8605a..5bbddd48e9b8b 100644
--- a/src/plugins/visualizations/common/expression_functions/xy_dimension.ts
+++ b/src/plugins/visualizations/common/expression_functions/xy_dimension.ts
@@ -14,8 +14,8 @@ import type {
ExpressionValueBoxed,
Datatable,
DatatableColumn,
- SerializedFieldFormat,
} from '../../../expressions/common';
+import type { SerializedFieldFormat } from '../../../field_formats/common';
export interface DateHistogramParams {
date: boolean;
diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts
index 47f544ce2f5d3..87095f5c389ed 100644
--- a/src/plugins/visualizations/public/plugin.ts
+++ b/src/plugins/visualizations/public/plugin.ts
@@ -23,9 +23,9 @@ import {
setAggs,
setChrome,
setOverlays,
- setSavedSearchLoader,
setEmbeddable,
setDocLinks,
+ setSpaces,
} from './services';
import {
VISUALIZE_EMBEDDABLE_TYPE,
@@ -51,8 +51,6 @@ import {
findListItems,
} from './utils/saved_visualize_utils';
-import { createSavedSearchesLoader } from '../../discover/public';
-
import type {
PluginInitializerContext,
CoreSetup,
@@ -191,6 +189,11 @@ export class VisualizationsPlugin
setAggs(data.search.aggs);
setOverlays(core.overlays);
setChrome(core.chrome);
+
+ if (spaces) {
+ setSpaces(spaces);
+ }
+
const savedVisualizationsLoader = createSavedVisLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
@@ -198,11 +201,7 @@ export class VisualizationsPlugin
visualizationTypes: types,
});
setSavedVisualizationsLoader(savedVisualizationsLoader);
- const savedSearchLoader = createSavedSearchesLoader({
- savedObjectsClient: core.savedObjects.client,
- savedObjects,
- });
- setSavedSearchLoader(savedSearchLoader);
+
return {
...types,
showNewVisModal,
diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts
index fbd8e414c2738..aa8183eb8da39 100644
--- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts
+++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts
@@ -16,8 +16,8 @@
import type { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public';
// @ts-ignore
import { updateOldState } from '../legacy/vis_update_state';
+import { __LEGACY } from '../../../discover/public';
import { extractReferences, injectReferences } from '../utils/saved_visualization_references';
-import { createSavedSearchesLoader } from '../../../discover/public';
import type { SavedObjectsClientContract } from '../../../../core/public';
import type { IndexPatternsContract } from '../../../../plugins/data/public';
import type { ISavedVis } from '../types';
@@ -30,7 +30,7 @@ export interface SavedVisServices {
/** @deprecated **/
export function createSavedVisClass(services: SavedVisServices) {
- const savedSearch = createSavedSearchesLoader(services);
+ const savedSearch = __LEGACY.createSavedSearchesLoader(services);
class SavedVis extends services.savedObjects.SavedObjectClass {
public static type: string = 'visualization';
diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts
index f1ab9077cd207..b5db56e61ebe4 100644
--- a/src/plugins/visualizations/public/services.ts
+++ b/src/plugins/visualizations/public/services.ts
@@ -18,13 +18,14 @@ import type {
} from '../../../core/public';
import type { TypesStart } from './vis_types';
import { createGetterSetter } from '../../../plugins/kibana_utils/public';
-import { DataPublicPluginStart, TimefilterContract } from '../../../plugins/data/public';
-import { UsageCollectionSetup } from '../../../plugins/usage_collection/public';
-import { ExpressionsStart } from '../../../plugins/expressions/public';
-import { UiActionsStart } from '../../../plugins/ui_actions/public';
-import { SavedVisualizationsLoader } from './saved_visualizations';
-import { SavedObjectLoader } from '../../saved_objects/public';
-import { EmbeddableStart } from '../../embeddable/public';
+import type { DataPublicPluginStart, TimefilterContract } from '../../../plugins/data/public';
+import type { UsageCollectionSetup } from '../../../plugins/usage_collection/public';
+import type { ExpressionsStart } from '../../../plugins/expressions/public';
+import type { UiActionsStart } from '../../../plugins/ui_actions/public';
+import type { SavedVisualizationsLoader } from './saved_visualizations';
+import type { EmbeddableStart } from '../../embeddable/public';
+
+import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public';
export const [getUISettings, setUISettings] = createGetterSetter('UISettings');
@@ -64,5 +65,4 @@ export const [getOverlays, setOverlays] = createGetterSetter('Over
export const [getChrome, setChrome] = createGetterSetter('Chrome');
-export const [getSavedSearchLoader, setSavedSearchLoader] =
- createGetterSetter('savedSearchLoader');
+export const [getSpaces, setSpaces] = createGetterSetter('Spaces', false);
diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts
index dfab4ecfc3cd8..2a1e7f2c8c673 100644
--- a/src/plugins/visualizations/public/vis.ts
+++ b/src/plugins/visualizations/public/vis.ts
@@ -21,17 +21,19 @@ import { Assign } from '@kbn/utility-types';
import { i18n } from '@kbn/i18n';
import { PersistedState } from './persisted_state';
-import { getTypes, getAggs, getSearch, getSavedSearchLoader } from './services';
+import { getTypes, getAggs, getSearch, getSavedObjects, getSpaces } from './services';
import {
IAggConfigs,
IndexPattern,
ISearchSource,
AggConfigSerialized,
SearchSourceFields,
-} from '../../../plugins/data/public';
+} from '../../data/public';
import { BaseVisType } from './vis_types';
import { VisParams } from '../common/types';
+import { getSavedSearch, throwErrorOnSavedSearchUrlConflict } from '../../discover/public';
+
export interface SerializedVisData {
expression?: string;
aggs: AggConfigSerialized[];
@@ -58,14 +60,20 @@ export interface VisData {
}
const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => {
- const searchSource = inputSearchSource.createCopy();
if (savedSearchId) {
- const savedSearch = await getSavedSearchLoader().get(savedSearchId);
+ const savedSearch = await getSavedSearch(savedSearchId, {
+ search: getSearch(),
+ savedObjectsClient: getSavedObjects().client,
+ spaces: getSpaces(),
+ });
+
+ await throwErrorOnSavedSearchUrlConflict(savedSearch);
- searchSource.setParent(savedSearch.searchSource);
+ if (savedSearch?.searchSource) {
+ inputSearchSource.setParent(savedSearch.searchSource);
+ }
}
- searchSource.setField('size', 0);
- return searchSource;
+ return inputSearchSource;
};
type PartialVisState = Assign }>;
diff --git a/src/plugins/visualizations/public/vis_schemas.ts b/src/plugins/visualizations/public/vis_schemas.ts
index 115e13ece45ff..f80f85fb55a60 100644
--- a/src/plugins/visualizations/public/vis_schemas.ts
+++ b/src/plugins/visualizations/public/vis_schemas.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { SerializedFieldFormat } from '../../expressions/public';
+import type { SerializedFieldFormat } from '../../field_formats/common';
import { IAggConfig, search } from '../../data/public';
import { Vis, VisToExpressionAstParams } from './types';
diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts
index 4debd9a4a7b7d..e77520c962d88 100644
--- a/src/plugins/visualize/public/application/types.ts
+++ b/src/plugins/visualize/public/application/types.ts
@@ -8,7 +8,6 @@
import type { EventEmitter } from 'events';
import type { History } from 'history';
-
import type { SerializableRecord } from '@kbn/utility-types';
import type {
@@ -38,7 +37,7 @@ import type {
import type { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
import type { Query, Filter, DataPublicPluginStart, TimeRange } from 'src/plugins/data/public';
import type { SharePluginStart } from 'src/plugins/share/public';
-import type { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public';
+import type { SavedObjectsStart } from 'src/plugins/saved_objects/public';
import type { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
import type { UrlForwardingStart } from 'src/plugins/url_forwarding/public';
import type { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public';
@@ -46,6 +45,7 @@ import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/pub
import type { DashboardStart } from '../../../dashboard/public';
import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
import type { UsageCollectionStart } from '../../../usage_collection/public';
+import type { SavedSearch } from '../../../discover/public';
import { PureVisState } from '../../common/types';
@@ -108,20 +108,15 @@ export interface VisualizeServices extends CoreStart {
spaces?: SpacesPluginStart;
}
-export interface SavedVisInstance {
- vis: Vis;
- savedVis: VisSavedObject;
- savedSearch?: SavedObject;
- embeddableHandler: VisualizeEmbeddableContract;
-}
-
-export interface ByValueVisInstance {
+export interface VisInstance {
vis: Vis;
savedVis: VisSavedObject;
- savedSearch?: SavedObject;
+ savedSearch?: SavedSearch;
embeddableHandler: VisualizeEmbeddableContract;
}
+export type SavedVisInstance = VisInstance;
+export type ByValueVisInstance = VisInstance;
export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance;
export type VisEditorConstructor = new (
@@ -142,7 +137,7 @@ export interface EditorRenderProps {
filters: Filter[];
timeRange: TimeRange;
query?: Query;
- savedSearch?: SavedObject;
+ savedSearch?: SavedSearch;
uiState: PersistedState;
/**
* Flag to determine if visualiztion is linked to the saved search
diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts
index 209516793d69d..777ba244c06a1 100644
--- a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts
+++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { createSavedSearchesLoader } from '../../../../discover/public';
+import { getSavedSearch } from '../../../../discover/public';
import type {
VisualizeInput,
VisSavedObject,
@@ -21,13 +21,13 @@ import { createVisualizeServicesMock } from './mocks';
import { VisualizeServices } from '../types';
import { BehaviorSubject } from 'rxjs';
-const mockSavedSearchObj = {};
-const mockGetSavedSearch = jest.fn(() => mockSavedSearchObj);
-
jest.mock('../../../../discover/public', () => ({
- createSavedSearchesLoader: jest.fn(() => ({
- get: mockGetSavedSearch,
- })),
+ getSavedSearch: jest.fn().mockResolvedValue({
+ id: 'savedSearch',
+ title: 'savedSearchTitle',
+ searchSource: {},
+ }),
+ throwErrorOnSavedSearchUrlConflict: jest.fn(),
}));
let savedVisMock: VisSavedObject;
@@ -116,9 +116,14 @@ describe('getVisualizationInstance', () => {
visMock.data.savedSearchId = 'saved_search_id';
const { savedSearch } = await getVisualizationInstance(mockServices, 'saved_vis_id');
- expect(createSavedSearchesLoader).toHaveBeenCalled();
- expect(mockGetSavedSearch).toHaveBeenCalledWith(visMock.data.savedSearchId);
- expect(savedSearch).toBe(mockSavedSearchObj);
+ expect(getSavedSearch).toHaveBeenCalled();
+ expect(savedSearch).toMatchInlineSnapshot(`
+ Object {
+ "id": "savedSearch",
+ "searchSource": Object {},
+ "title": "savedSearchTitle",
+ }
+ `);
});
test('should subscribe on embeddable handler updates and send toasts on errors', async () => {
diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
index faf25ff28cec0..876501d5f099b 100644
--- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
+++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
@@ -14,10 +14,13 @@ import {
VisualizeInput,
} from 'src/plugins/visualizations/public';
import { SearchSourceFields } from 'src/plugins/data/public';
-import { SavedObject } from 'src/plugins/saved_objects/public';
import { cloneDeep } from 'lodash';
import { ExpressionValueError } from 'src/plugins/expressions/public';
-import { createSavedSearchesLoader } from '../../../../discover/public';
+import {
+ getSavedSearch,
+ SavedSearch,
+ throwErrorOnSavedSearchUrlConflict,
+} from '../../../../discover/public';
import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common';
import { VisualizeServices } from '../types';
@@ -33,8 +36,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
vis: Vis,
visualizeServices: VisualizeServices
) => {
- const { data, createVisEmbeddableFromObject, savedObjects, savedObjectsPublic } =
- visualizeServices;
+ const { data, createVisEmbeddableFromObject, savedObjects, spaces } = visualizeServices;
const embeddableHandler = (await createVisEmbeddableFromObject(vis, {
id: '',
timeRange: data.query.timefilter.timefilter.getTime(),
@@ -50,13 +52,16 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
}
});
- let savedSearch: SavedObject | undefined;
+ let savedSearch: SavedSearch | undefined;
if (vis.data.savedSearchId) {
- savedSearch = await createSavedSearchesLoader({
+ savedSearch = await getSavedSearch(vis.data.savedSearchId, {
+ search: data.search,
savedObjectsClient: savedObjects.client,
- savedObjects: savedObjectsPublic,
- }).get(vis.data.savedSearchId);
+ spaces,
+ });
+
+ await throwErrorOnSavedSearchUrlConflict(savedSearch);
}
return { savedSearch, embeddableHandler };
diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts
index c9df6a6ec57d8..7ff3434286b6b 100644
--- a/src/plugins/visualize/public/plugin.ts
+++ b/src/plugins/visualize/public/plugin.ts
@@ -28,7 +28,6 @@ import {
createKbnUrlStateStorage,
withNotifyOnErrors,
} from '../../kibana_utils/public';
-import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public';
import { VisualizeConstants } from './application/visualize_constants';
import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';
@@ -45,6 +44,7 @@ import type { EmbeddableStart } from '../../embeddable/public';
import type { DashboardStart } from '../../dashboard/public';
import type { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public';
import type { UsageCollectionStart } from '../../usage_collection/public';
+import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public';
import { setVisEditorsRegistry, setUISettings, setUsageCollector } from './services';
import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry';
@@ -62,7 +62,7 @@ export interface VisualizePluginStartDependencies {
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
presentationUtil: PresentationUtilPluginStart;
usageCollection?: UsageCollectionStart;
- spaces: SpacesPluginStart;
+ spaces?: SpacesApi;
}
export interface VisualizePluginSetupDependencies {
diff --git a/test/examples/embeddables/dashboard.ts b/test/examples/embeddables/dashboard.ts
index 77ad5a5da9eeb..b97905ca9ce6a 100644
--- a/test/examples/embeddables/dashboard.ts
+++ b/test/examples/embeddables/dashboard.ts
@@ -95,10 +95,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const pieChart = getService('pieChart');
- const browser = getService('browser');
const dashboardExpect = getService('dashboardExpect');
const elasticChart = getService('elasticChart');
const PageObjects = getPageObjects(['common', 'visChart']);
+ const monacoEditor = getService('monacoEditor');
describe('dashboard container', () => {
before(async () => {
@@ -128,17 +128,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
});
async function updateInput(input: string) {
- const editorWrapper = await (
- await testSubjects.find('dashboardEmbeddableByValueInputEditor')
- ).findByClassName('ace_editor');
- const editorId = await editorWrapper.getAttribute('id');
- await browser.execute(
- (_editorId: string, _input: string) => {
- return (window as any).ace.edit(_editorId).setValue(_input);
- },
- editorId,
- input
- );
+ await monacoEditor.setCodeEditorValue(input);
await testSubjects.click('dashboardEmbeddableByValueInputSubmit');
}
}
diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts
index 45ba62749dd77..0cc0fa4806482 100644
--- a/test/functional/apps/dashboard/dashboard_state.ts
+++ b/test/functional/apps/dashboard/dashboard_state.ts
@@ -7,6 +7,7 @@
*/
import expect from '@kbn/expect';
+import chroma from 'chroma-js';
import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../page_objects/dashboard_page';
import { DEFAULT_PANEL_WIDTH } from '../../../../src/plugins/dashboard/public/application/embeddable/dashboard_constants';
@@ -264,14 +265,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async () => {
- const allPieSlicesColor = await pieChart.getAllPieSliceStyles('80,000');
- let whitePieSliceCounts = 0;
- allPieSlicesColor.forEach((style) => {
- if (style.indexOf('rgb(255, 255, 255)') > -1) {
- whitePieSliceCounts++;
- }
- });
-
+ const allPieSlicesColor = await pieChart.getAllPieSliceColor('80,000');
+ const whitePieSliceCounts = allPieSlicesColor.reduce((count, color) => {
+ // converting the color to a common format, testing the color, not the string format
+ return chroma(color).hex().toUpperCase() === '#FFFFFF' ? count + 1 : count;
+ }, 0);
expect(whitePieSliceCounts).to.be(1);
});
});
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index 2645148467d58..b929a78072868 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -21,6 +21,7 @@ export class SettingsPageObject extends FtrService {
private readonly header = this.ctx.getPageObject('header');
private readonly common = this.ctx.getPageObject('common');
private readonly savedObjects = this.ctx.getPageObject('savedObjects');
+ private readonly monacoEditor = this.ctx.getService('monacoEditor');
async clickNavigation() {
await this.find.clickDisplayedByCssSelector('.app-link:nth-child(5) a');
@@ -725,14 +726,7 @@ export class SettingsPageObject extends FtrService {
async setScriptedFieldScript(script: string) {
this.log.debug('set scripted field script = ' + script);
- const aceEditorCssSelector = '[data-test-subj="editorFieldScript"] .ace_editor';
- const editor = await this.find.byCssSelector(aceEditorCssSelector);
- await editor.click();
- const existingText = await editor.getVisibleText();
- for (let i = 0; i < existingText.length; i++) {
- await this.browser.pressKeys(this.browser.keys.BACK_SPACE);
- }
- await this.browser.pressKeys(...script.split(''));
+ await this.monacoEditor.setCodeEditorValue(script);
}
async openScriptedFieldHelp(activeTab: string) {
diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts
index d2e4091f93577..b0e9e21d07b0b 100644
--- a/test/functional/page_objects/visualize_chart_page.ts
+++ b/test/functional/page_objects/visualize_chart_page.ts
@@ -7,7 +7,7 @@
*/
import { Position } from '@elastic/charts';
-import Color from 'color';
+import chroma from 'chroma-js';
import { FtrService } from '../ftr_provider_context';
@@ -181,17 +181,17 @@ export class VisualizeChartPageObject extends FtrService {
return items.some(({ color: c }) => c === color);
}
- public async doesSelectedLegendColorExistForPie(color: string) {
+ public async doesSelectedLegendColorExistForPie(matchingColor: string) {
if (await this.isNewLibraryChart(pieChartSelector)) {
+ const hexMatchingColor = chroma(matchingColor).hex().toUpperCase();
const slices =
(await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? [];
- return slices.some(({ color: c }) => {
- const rgbColor = new Color(color).rgb().toString();
- return c === rgbColor;
+ return slices.some(({ color }) => {
+ return hexMatchingColor === chroma(color).hex().toUpperCase();
});
}
- return await this.testSubjects.exists(`legendSelectedColor-${color}`);
+ return await this.testSubjects.exists(`legendSelectedColor-${matchingColor}`);
}
public async expectError() {
diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts
index 7c925318f0211..ff0c24e2830cf 100644
--- a/test/functional/services/visualizations/pie_chart.ts
+++ b/test/functional/services/visualizations/pie_chart.ts
@@ -7,6 +7,7 @@
*/
import expect from '@kbn/expect';
+import { isNil } from 'lodash';
import { FtrService } from '../../ftr_provider_context';
const pieChartSelector = 'visTypePieChart';
@@ -100,8 +101,8 @@ export class PieChartService extends FtrService {
return await pieSlice.getAttribute('style');
}
- async getAllPieSliceStyles(name: string) {
- this.log.debug(`VisualizePage.getAllPieSliceStyles(${name})`);
+ async getAllPieSliceColor(name: string) {
+ this.log.debug(`VisualizePage.getAllPieSliceColor(${name})`);
if (await this.visChart.isNewLibraryChart(pieChartSelector)) {
const slices =
(await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ??
@@ -112,9 +113,22 @@ export class PieChartService extends FtrService {
return selectedSlice.map((slice) => slice.color);
}
const pieSlices = await this.getAllPieSlices(name);
- return await Promise.all(
+ const slicesStyles = await Promise.all(
pieSlices.map(async (pieSlice) => await pieSlice.getAttribute('style'))
);
+ return slicesStyles
+ .map(
+ (styles) =>
+ styles.split(';').reduce>((styleAsObj, style) => {
+ const stylePair = style.split(':');
+ if (stylePair.length !== 2) {
+ return styleAsObj;
+ }
+ styleAsObj[stylePair[0].trim()] = stylePair[1].trim();
+ return styleAsObj;
+ }, {}).fill // in vislib the color is available on the `fill` style prop
+ )
+ .filter((d) => !isNil(d));
}
async getPieChartData() {
diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx
index a34cd0ab518de..3e2f08fc89c7b 100644
--- a/x-pack/examples/reporting_example/public/components/app.tsx
+++ b/x-pack/examples/reporting_example/public/components/app.tsx
@@ -244,7 +244,12 @@ export const ReportingExampleApp = ({
)}
{logos.map((item, index) => (
-
+
}
title={`Elastic ${item}`}
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index b19e89a599840..0d66c9d30f8b9 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -33,29 +33,36 @@ Table of Contents
- [actionsClient.execute(options)](#actionsclientexecuteoptions)
- [Example](#example-2)
- [Built-in Action Types](#built-in-action-types)
- - [ServiceNow](#servicenow)
+ - [ServiceNow ITSM](#servicenow-itsm)
- [`params`](#params)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice)
- [`subActionParams (getFields)`](#subactionparams-getfields)
- [`subActionParams (getIncident)`](#subactionparams-getincident)
- [`subActionParams (getChoices)`](#subactionparams-getchoices)
- - [Jira](#jira)
+ - [ServiceNow Sec Ops](#servicenow-sec-ops)
- [`params`](#params-1)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1)
+ - [`subActionParams (getFields)`](#subactionparams-getfields-1)
- [`subActionParams (getIncident)`](#subactionparams-getincident-1)
+ - [`subActionParams (getChoices)`](#subactionparams-getchoices-1)
+ - [| fields | An array of fields. Example: `[priority, category]`. | string[] |](#-fields----an-array-of-fields-example-priority-category--string-)
+ - [Jira](#jira)
+ - [`params`](#params-2)
+ - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
+ - [`subActionParams (getIncident)`](#subactionparams-getincident-2)
- [`subActionParams (issueTypes)`](#subactionparams-issuetypes)
- [`subActionParams (fieldsByIssueType)`](#subactionparams-fieldsbyissuetype)
- [`subActionParams (issues)`](#subactionparams-issues)
- [`subActionParams (issue)`](#subactionparams-issue)
- - [`subActionParams (getFields)`](#subactionparams-getfields-1)
- - [IBM Resilient](#ibm-resilient)
- - [`params`](#params-2)
- - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
- [`subActionParams (getFields)`](#subactionparams-getfields-2)
+ - [IBM Resilient](#ibm-resilient)
+ - [`params`](#params-3)
+ - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3)
+ - [`subActionParams (getFields)`](#subactionparams-getfields-3)
- [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes)
- [`subActionParams (severity)`](#subactionparams-severity)
- [Swimlane](#swimlane)
- - [`params`](#params-3)
+ - [`params`](#params-4)
- [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-)
- [Command Line Utility](#command-line-utility)
- [Developing New Action Types](#developing-new-action-types)
@@ -246,9 +253,9 @@ Kibana ships with a set of built-in action types. See [Actions and connector typ
In addition to the documented configurations, several built in action type offer additional `params` configurations.
-## ServiceNow
+## ServiceNow ITSM
-The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available.
+The [ServiceNow ITSM user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available.
### `params`
| Property | Description | Type |
@@ -265,16 +272,18 @@ The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kib
The following table describes the properties of the `incident` object.
-| Property | Description | Type |
-| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- |
-| short_description | The title of the incident. | string |
-| description | The description of the incident. | string _(optional)_ |
-| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
-| severity | The severity in ServiceNow. | string _(optional)_ |
-| urgency | The urgency in ServiceNow. | string _(optional)_ |
-| impact | The impact in ServiceNow. | string _(optional)_ |
-| category | The category in ServiceNow. | string _(optional)_ |
-| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
+| Property | Description | Type |
+| ------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- |
+| short_description | The title of the incident. | string |
+| description | The description of the incident. | string _(optional)_ |
+| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ |
+| severity | The severity in ServiceNow. | string _(optional)_ |
+| urgency | The urgency in ServiceNow. | string _(optional)_ |
+| impact | The impact in ServiceNow. | string _(optional)_ |
+| category | The category in ServiceNow. | string _(optional)_ |
+| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
+| correlation_id | The correlation id of the incident. | string _(optional)_ |
+| correlation_display | The correlation display of the ServiceNow. | string _(optional)_ |
#### `subActionParams (getFields)`
@@ -289,12 +298,64 @@ No parameters for the `getFields` subaction. Provide an empty object `{}`.
#### `subActionParams (getChoices)`
-| Property | Description | Type |
-| -------- | ------------------------------------------------------------ | -------- |
-| fields | An array of fields. Example: `[priority, category, impact]`. | string[] |
+| Property | Description | Type |
+| -------- | -------------------------------------------------- | -------- |
+| fields | An array of fields. Example: `[category, impact]`. | string[] |
---
+## ServiceNow Sec Ops
+
+The [ServiceNow SecOps user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-sir-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available.
+
+### `params`
+
+| Property | Description | Type |
+| --------------- | -------------------------------------------------------------------------------------------------- | ------ |
+| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `getIncident`, and `getChoices`. | string |
+| subActionParams | The parameters of the subaction. | object |
+
+#### `subActionParams (pushToService)`
+
+| Property | Description | Type |
+| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- |
+| incident | The ServiceNow security incident. | object |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ |
+
+The following table describes the properties of the `incident` object.
+
+| Property | Description | Type |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
+| short_description | The title of the security incident. | string |
+| description | The description of the security incident. | string _(optional)_ |
+| externalId | The ID of the security incident in ServiceNow. If present, the security incident is updated. Otherwise, a new security incident is created. | string _(optional)_ |
+| priority | The priority in ServiceNow. | string _(optional)_ |
+| dest_ip | A list of destination IPs related to the security incident. The IPs will be added as observables to the security incident. | (string \| string[]) _(optional)_ |
+| source_ip | A list of source IPs related to the security incident. The IPs will be added as observables to the security incident. | (string \| string[]) _(optional)_ |
+| malware_hash | A list of malware hashes related to the security incident. The hashes will be added as observables to the security incident. | (string \| string[]) _(optional)_ |
+| malware_url | A list of malware URLs related to the security incident. The URLs will be added as observables to the security incident. | (string \| string[]) _(optional)_ |
+| category | The category in ServiceNow. | string _(optional)_ |
+| subcategory | The subcategory in ServiceNow. | string _(optional)_ |
+| correlation_id | The correlation id of the security incident. | string _(optional)_ |
+| correlation_display | The correlation display of the security incident. | string _(optional)_ |
+
+#### `subActionParams (getFields)`
+
+No parameters for the `getFields` subaction. Provide an empty object `{}`.
+
+#### `subActionParams (getIncident)`
+
+| Property | Description | Type |
+| ---------- | ---------------------------------------------- | ------ |
+| externalId | The ID of the security incident in ServiceNow. | string |
+
+
+#### `subActionParams (getChoices)`
+
+| Property | Description | Type |
+| -------- | ---------------------------------------------------- | -------- |
+| fields | An array of fields. Example: `[priority, category]`. | string[] |
+---
## Jira
The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/master/jira-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available.
diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts
index 5d83b658111e4..7710ff79d08b4 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts
@@ -143,7 +143,7 @@ export function getActionType({
}),
validate: {
config: schema.object(configSchemaProps, {
- validate: curry(valdiateActionTypeConfig)(configurationUtilities),
+ validate: curry(validateActionTypeConfig)(configurationUtilities),
}),
secrets: SecretsSchema,
params: ParamsSchema,
@@ -152,7 +152,7 @@ export function getActionType({
};
}
-function valdiateActionTypeConfig(
+function validateActionTypeConfig(
configurationUtilities: ActionsConfigurationUtilities,
configObject: ActionTypeConfigType
) {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
index 8d24e48d4d515..e1f66263729e2 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
@@ -25,6 +25,7 @@ describe('api', () => {
const res = await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -57,6 +58,7 @@ describe('api', () => {
const res = await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -78,6 +80,7 @@ describe('api', () => {
await api.pushToService({
externalService,
params,
+ config: {},
secrets: { username: 'elastic', password: 'elastic' },
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -93,6 +96,9 @@ describe('api', () => {
caller_id: 'elastic',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
+ opened_by: 'elastic',
},
});
expect(externalService.updateIncident).not.toHaveBeenCalled();
@@ -103,6 +109,7 @@ describe('api', () => {
await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -118,6 +125,8 @@ describe('api', () => {
comments: 'A comment',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-1',
});
@@ -132,6 +141,8 @@ describe('api', () => {
comments: 'Another comment',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-1',
});
@@ -142,6 +153,7 @@ describe('api', () => {
await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'work_notes',
@@ -157,6 +169,8 @@ describe('api', () => {
work_notes: 'A comment',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-1',
});
@@ -171,6 +185,8 @@ describe('api', () => {
work_notes: 'Another comment',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-1',
});
@@ -182,6 +198,7 @@ describe('api', () => {
const res = await api.pushToService({
externalService,
params: apiParams,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -210,6 +227,7 @@ describe('api', () => {
const res = await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -228,6 +246,7 @@ describe('api', () => {
await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -243,6 +262,8 @@ describe('api', () => {
subcategory: 'os',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
});
expect(externalService.createIncident).not.toHaveBeenCalled();
@@ -253,6 +274,7 @@ describe('api', () => {
await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
@@ -267,6 +289,8 @@ describe('api', () => {
subcategory: 'os',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-3',
});
@@ -281,6 +305,8 @@ describe('api', () => {
comments: 'A comment',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-2',
});
@@ -291,6 +317,7 @@ describe('api', () => {
await api.pushToService({
externalService,
params,
+ config: {},
secrets: {},
logger: mockedLogger,
commentFieldKey: 'work_notes',
@@ -305,6 +332,8 @@ describe('api', () => {
subcategory: 'os',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-3',
});
@@ -319,6 +348,8 @@ describe('api', () => {
work_notes: 'A comment',
description: 'Incident description',
short_description: 'Incident title',
+ correlation_display: 'Alerting',
+ correlation_id: 'ruleId',
},
incidentId: 'incident-2',
});
@@ -344,4 +375,23 @@ describe('api', () => {
expect(res).toEqual(serviceNowChoices);
});
});
+
+ describe('getIncident', () => {
+ test('it gets the incident correctly', async () => {
+ const res = await api.getIncident({
+ externalService,
+ params: {
+ externalId: 'incident-1',
+ },
+ });
+ expect(res).toEqual({
+ description: 'description from servicenow',
+ id: 'incident-1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ short_description: 'title from servicenow',
+ title: 'INC01',
+ url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
index 4120c07c32303..88cdfd069cf1b 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
@@ -6,7 +6,7 @@
*/
import {
- ExternalServiceApi,
+ ExternalServiceAPI,
GetChoicesHandlerArgs,
GetChoicesResponse,
GetCommonFieldsHandlerArgs,
@@ -19,7 +19,11 @@ import {
} from './types';
const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {};
-const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {};
+const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {
+ const { externalId: id } = params;
+ const res = await externalService.getIncident(id);
+ return res;
+};
const pushToServiceHandler = async ({
externalService,
@@ -42,6 +46,7 @@ const pushToServiceHandler = async ({
incident: {
...incident,
caller_id: secrets.username,
+ opened_by: secrets.username,
},
});
}
@@ -84,7 +89,7 @@ const getChoicesHandler = async ({
return res;
};
-export const api: ExternalServiceApi = {
+export const api: ExternalServiceAPI = {
getChoices: getChoicesHandler,
getFields: getFieldsHandler,
getIncident: getIncidentHandler,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts
new file mode 100644
index 0000000000000..358af7cd2e9ef
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts
@@ -0,0 +1,286 @@
+/*
+ * 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 { Logger } from '../../../../../../src/core/server';
+import { externalServiceSIRMock, sirParams } from './mocks';
+import { ExternalServiceSIR, ObservableTypes } from './types';
+import { apiSIR, combineObservables, formatObservables, prepareParams } from './api_sir';
+let mockedLogger: jest.Mocked;
+
+describe('api_sir', () => {
+ let externalService: jest.Mocked;
+
+ beforeEach(() => {
+ externalService = externalServiceSIRMock.create();
+ jest.clearAllMocks();
+ });
+
+ describe('combineObservables', () => {
+ test('it returns an empty array when both arguments are an empty array', async () => {
+ expect(combineObservables([], [])).toEqual([]);
+ });
+
+ test('it returns an empty array when both arguments are an empty string', async () => {
+ expect(combineObservables('', '')).toEqual([]);
+ });
+
+ test('it returns an empty array when a="" and b=[]', async () => {
+ expect(combineObservables('', [])).toEqual([]);
+ });
+
+ test('it returns an empty array when a=[] and b=""', async () => {
+ expect(combineObservables([], '')).toEqual([]);
+ });
+
+ test('it returns a if b is empty', async () => {
+ expect(combineObservables('a', '')).toEqual(['a']);
+ });
+
+ test('it returns b if a is empty', async () => {
+ expect(combineObservables([], ['b'])).toEqual(['b']);
+ });
+
+ test('it combines two strings', async () => {
+ expect(combineObservables('a,b', 'c,d')).toEqual(['a', 'b', 'c', 'd']);
+ });
+
+ test('it combines two arrays', async () => {
+ expect(combineObservables(['a'], ['b'])).toEqual(['a', 'b']);
+ });
+
+ test('it combines a string with an array', async () => {
+ expect(combineObservables('a', ['b'])).toEqual(['a', 'b']);
+ });
+
+ test('it combines an array with a string ', async () => {
+ expect(combineObservables(['a'], 'b')).toEqual(['a', 'b']);
+ });
+
+ test('it combines a "," concatenated string', async () => {
+ expect(combineObservables(['a'], 'b,c,d')).toEqual(['a', 'b', 'c', 'd']);
+ expect(combineObservables('b,c,d', ['a'])).toEqual(['b', 'c', 'd', 'a']);
+ });
+
+ test('it combines a "|" concatenated string', async () => {
+ expect(combineObservables(['a'], 'b|c|d')).toEqual(['a', 'b', 'c', 'd']);
+ expect(combineObservables('b|c|d', ['a'])).toEqual(['b', 'c', 'd', 'a']);
+ });
+
+ test('it combines a space concatenated string', async () => {
+ expect(combineObservables(['a'], 'b c d')).toEqual(['a', 'b', 'c', 'd']);
+ expect(combineObservables('b c d', ['a'])).toEqual(['b', 'c', 'd', 'a']);
+ });
+
+ test('it combines a "\\n" concatenated string', async () => {
+ expect(combineObservables(['a'], 'b\nc\nd')).toEqual(['a', 'b', 'c', 'd']);
+ expect(combineObservables('b\nc\nd', ['a'])).toEqual(['b', 'c', 'd', 'a']);
+ });
+
+ test('it combines a "\\r" concatenated string', async () => {
+ expect(combineObservables(['a'], 'b\rc\rd')).toEqual(['a', 'b', 'c', 'd']);
+ expect(combineObservables('b\rc\rd', ['a'])).toEqual(['b', 'c', 'd', 'a']);
+ });
+
+ test('it combines a "\\t" concatenated string', async () => {
+ expect(combineObservables(['a'], 'b\tc\td')).toEqual(['a', 'b', 'c', 'd']);
+ expect(combineObservables('b\tc\td', ['a'])).toEqual(['b', 'c', 'd', 'a']);
+ });
+
+ test('it combines two strings with different delimiter', async () => {
+ expect(combineObservables('a|b|c', 'd e f')).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
+ });
+ });
+
+ describe('formatObservables', () => {
+ test('it formats array observables correctly', async () => {
+ const expectedTypes: Array<[ObservableTypes, string]> = [
+ [ObservableTypes.ip4, 'ipv4-addr'],
+ [ObservableTypes.sha256, 'SHA256'],
+ [ObservableTypes.url, 'URL'],
+ ];
+
+ for (const type of expectedTypes) {
+ expect(formatObservables(['a', 'b', 'c'], type[0])).toEqual([
+ { type: type[1], value: 'a' },
+ { type: type[1], value: 'b' },
+ { type: type[1], value: 'c' },
+ ]);
+ }
+ });
+
+ test('it removes duplicates from array observables correctly', async () => {
+ expect(formatObservables(['a', 'a', 'c'], ObservableTypes.ip4)).toEqual([
+ { type: 'ipv4-addr', value: 'a' },
+ { type: 'ipv4-addr', value: 'c' },
+ ]);
+ });
+
+ test('it formats an empty array correctly', async () => {
+ expect(formatObservables([], ObservableTypes.ip4)).toEqual([]);
+ });
+
+ test('it removes empty observables correctly', async () => {
+ expect(formatObservables(['a', '', 'c'], ObservableTypes.ip4)).toEqual([
+ { type: 'ipv4-addr', value: 'a' },
+ { type: 'ipv4-addr', value: 'c' },
+ ]);
+ });
+ });
+
+ describe('prepareParams', () => {
+ test('it prepares the params correctly when the connector is legacy', async () => {
+ expect(prepareParams(true, sirParams)).toEqual({
+ ...sirParams,
+ incident: {
+ ...sirParams.incident,
+ dest_ip: '192.168.1.1,192.168.1.3',
+ source_ip: '192.168.1.2,192.168.1.4',
+ malware_hash: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9',
+ malware_url: 'https://example.com',
+ },
+ });
+ });
+
+ test('it prepares the params correctly when the connector is not legacy', async () => {
+ expect(prepareParams(false, sirParams)).toEqual({
+ ...sirParams,
+ incident: {
+ ...sirParams.incident,
+ dest_ip: null,
+ source_ip: null,
+ malware_hash: null,
+ malware_url: null,
+ },
+ });
+ });
+
+ test('it prepares the params correctly when the connector is legacy and the observables are undefined', async () => {
+ const {
+ dest_ip: destIp,
+ source_ip: sourceIp,
+ malware_hash: malwareHash,
+ malware_url: malwareURL,
+ ...incidentWithoutObservables
+ } = sirParams.incident;
+
+ expect(
+ prepareParams(true, {
+ ...sirParams,
+ // @ts-expect-error
+ incident: incidentWithoutObservables,
+ })
+ ).toEqual({
+ ...sirParams,
+ incident: {
+ ...sirParams.incident,
+ dest_ip: null,
+ source_ip: null,
+ malware_hash: null,
+ malware_url: null,
+ },
+ });
+ });
+ });
+
+ describe('pushToService', () => {
+ test('it creates an incident correctly', async () => {
+ const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } };
+ const res = await apiSIR.pushToService({
+ externalService,
+ params,
+ config: { isLegacy: false },
+ secrets: {},
+ logger: mockedLogger,
+ commentFieldKey: 'work_notes',
+ });
+
+ expect(res).toEqual({
+ id: 'incident-1',
+ title: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ },
+ {
+ commentId: 'case-comment-2',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ },
+ ],
+ });
+ });
+
+ test('it adds observables correctly', async () => {
+ const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } };
+ await apiSIR.pushToService({
+ externalService,
+ params,
+ config: { isLegacy: false },
+ secrets: {},
+ logger: mockedLogger,
+ commentFieldKey: 'work_notes',
+ });
+
+ expect(externalService.bulkAddObservableToIncident).toHaveBeenCalledWith(
+ [
+ { type: 'ipv4-addr', value: '192.168.1.1' },
+ { type: 'ipv4-addr', value: '192.168.1.3' },
+ { type: 'ipv4-addr', value: '192.168.1.2' },
+ { type: 'ipv4-addr', value: '192.168.1.4' },
+ {
+ type: 'SHA256',
+ value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9',
+ },
+ { type: 'URL', value: 'https://example.com' },
+ ],
+ // createIncident mock returns this incident id
+ 'incident-1'
+ );
+ });
+
+ test('it does not call bulkAddObservableToIncident if it a legacy connector', async () => {
+ const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } };
+ await apiSIR.pushToService({
+ externalService,
+ params,
+ config: { isLegacy: true },
+ secrets: {},
+ logger: mockedLogger,
+ commentFieldKey: 'work_notes',
+ });
+
+ expect(externalService.bulkAddObservableToIncident).not.toHaveBeenCalled();
+ });
+
+ test('it does not call bulkAddObservableToIncident if there are no observables', async () => {
+ const params = {
+ ...sirParams,
+ incident: {
+ ...sirParams.incident,
+ dest_ip: null,
+ source_ip: null,
+ malware_hash: null,
+ malware_url: null,
+ externalId: null,
+ },
+ };
+
+ await apiSIR.pushToService({
+ externalService,
+ params,
+ config: { isLegacy: false },
+ secrets: {},
+ logger: mockedLogger,
+ commentFieldKey: 'work_notes',
+ });
+
+ expect(externalService.bulkAddObservableToIncident).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts
new file mode 100644
index 0000000000000..326bb79a0e708
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts
@@ -0,0 +1,154 @@
+/*
+ * 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 { isEmpty, isString } from 'lodash';
+
+import {
+ ExecutorSubActionPushParamsSIR,
+ ExternalServiceAPI,
+ ExternalServiceSIR,
+ ObservableTypes,
+ PushToServiceApiHandlerArgs,
+ PushToServiceApiParamsSIR,
+ PushToServiceResponse,
+} from './types';
+
+import { api } from './api';
+
+const SPLIT_REGEX = /[ ,|\r\n\t]+/;
+
+export const formatObservables = (observables: string[], type: ObservableTypes) => {
+ /**
+ * ServiceNow accepted formats are: comma, new line, tab, or pipe separators.
+ * Before the application the observables were being sent to ServiceNow as a concatenated string with
+ * delimiter. With the application the format changed to an array of observables.
+ */
+ const uniqueObservables = new Set(observables);
+ return [...uniqueObservables].filter((obs) => !isEmpty(obs)).map((obs) => ({ value: obs, type }));
+};
+
+const obsAsArray = (obs: string | string[]): string[] => {
+ if (isEmpty(obs)) {
+ return [];
+ }
+
+ if (isString(obs)) {
+ return obs.split(SPLIT_REGEX);
+ }
+
+ return obs;
+};
+
+export const combineObservables = (a: string | string[], b: string | string[]): string[] => {
+ const first = obsAsArray(a);
+ const second = obsAsArray(b);
+
+ return [...first, ...second];
+};
+
+const observablesToString = (obs: string | string[] | null | undefined): string | null => {
+ if (Array.isArray(obs)) {
+ return obs.join(',');
+ }
+
+ return obs ?? null;
+};
+
+export const prepareParams = (
+ isLegacy: boolean,
+ params: PushToServiceApiParamsSIR
+): PushToServiceApiParamsSIR => {
+ if (isLegacy) {
+ /**
+ * The schema has change to accept an array of observables
+ * or a string. In the case of a legacy connector we need to
+ * convert the observables to a string
+ */
+ return {
+ ...params,
+ incident: {
+ ...params.incident,
+ dest_ip: observablesToString(params.incident.dest_ip),
+ malware_hash: observablesToString(params.incident.malware_hash),
+ malware_url: observablesToString(params.incident.malware_url),
+ source_ip: observablesToString(params.incident.source_ip),
+ },
+ };
+ }
+
+ /**
+ * For non legacy connectors the observables
+ * will be added in a different call.
+ * They need to be set to null when sending the fields
+ * to ServiceNow
+ */
+ return {
+ ...params,
+ incident: {
+ ...params.incident,
+ dest_ip: null,
+ malware_hash: null,
+ malware_url: null,
+ source_ip: null,
+ },
+ };
+};
+
+const pushToServiceHandler = async ({
+ externalService,
+ params,
+ config,
+ secrets,
+ commentFieldKey,
+ logger,
+}: PushToServiceApiHandlerArgs): Promise => {
+ const res = await api.pushToService({
+ externalService,
+ params: prepareParams(!!config.isLegacy, params as PushToServiceApiParamsSIR),
+ config,
+ secrets,
+ commentFieldKey,
+ logger,
+ });
+
+ const {
+ incident: {
+ dest_ip: destIP,
+ malware_hash: malwareHash,
+ malware_url: malwareUrl,
+ source_ip: sourceIP,
+ },
+ } = params as ExecutorSubActionPushParamsSIR;
+
+ /**
+ * Add bulk observables is only available for new connectors
+ * Old connectors gonna add their observables
+ * through the pushToService call.
+ */
+
+ if (!config.isLegacy) {
+ const sirExternalService = externalService as ExternalServiceSIR;
+
+ const obsWithType: Array<[string[], ObservableTypes]> = [
+ [combineObservables(destIP ?? [], sourceIP ?? []), ObservableTypes.ip4],
+ [obsAsArray(malwareHash ?? []), ObservableTypes.sha256],
+ [obsAsArray(malwareUrl ?? []), ObservableTypes.url],
+ ];
+
+ const observables = obsWithType.map(([obs, type]) => formatObservables(obs, type)).flat();
+ if (observables.length > 0) {
+ await sirExternalService.bulkAddObservableToIncident(observables, res.id);
+ }
+ }
+
+ return res;
+};
+
+export const apiSIR: ExternalServiceAPI = {
+ ...api,
+ pushToService: pushToServiceHandler,
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts
new file mode 100644
index 0000000000000..babd360cbcb82
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { snExternalServiceConfig } from './config';
+
+/**
+ * The purpose of this test is to
+ * prevent developers from accidentally
+ * change important configuration values
+ * such as the scope or the import set table
+ * of our ServiceNow application
+ */
+
+describe('config', () => {
+ test('ITSM: the config are correct', async () => {
+ const snConfig = snExternalServiceConfig['.servicenow'];
+ expect(snConfig).toEqual({
+ importSetTable: 'x_elas2_inc_int_elastic_incident',
+ appScope: 'x_elas2_inc_int',
+ table: 'incident',
+ useImportAPI: true,
+ commentFieldKey: 'work_notes',
+ });
+ });
+
+ test('SIR: the config are correct', async () => {
+ const snConfig = snExternalServiceConfig['.servicenow-sir'];
+ expect(snConfig).toEqual({
+ importSetTable: 'x_elas2_sir_int_elastic_si_incident',
+ appScope: 'x_elas2_sir_int',
+ table: 'sn_si_incident',
+ useImportAPI: true,
+ commentFieldKey: 'work_notes',
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts
new file mode 100644
index 0000000000000..37e4c6994b403
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ ENABLE_NEW_SN_ITSM_CONNECTOR,
+ ENABLE_NEW_SN_SIR_CONNECTOR,
+} from '../../constants/connectors';
+import { SNProductsConfig } from './types';
+
+export const serviceNowITSMTable = 'incident';
+export const serviceNowSIRTable = 'sn_si_incident';
+
+export const ServiceNowITSMActionTypeId = '.servicenow';
+export const ServiceNowSIRActionTypeId = '.servicenow-sir';
+
+export const snExternalServiceConfig: SNProductsConfig = {
+ '.servicenow': {
+ importSetTable: 'x_elas2_inc_int_elastic_incident',
+ appScope: 'x_elas2_inc_int',
+ table: 'incident',
+ useImportAPI: ENABLE_NEW_SN_ITSM_CONNECTOR,
+ commentFieldKey: 'work_notes',
+ },
+ '.servicenow-sir': {
+ importSetTable: 'x_elas2_sir_int_elastic_si_incident',
+ appScope: 'x_elas2_sir_int',
+ table: 'sn_si_incident',
+ useImportAPI: ENABLE_NEW_SN_SIR_CONNECTOR,
+ commentFieldKey: 'work_notes',
+ },
+};
+
+export const FIELD_PREFIX = 'u_';
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
index f2b500df6ccb3..29907381d45da 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
@@ -18,7 +18,7 @@ import {
import { ActionsConfigurationUtilities } from '../../actions_config';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
import { createExternalService } from './service';
-import { api } from './api';
+import { api as commonAPI } from './api';
import * as i18n from './translations';
import { Logger } from '../../../../../../src/core/server';
import {
@@ -30,7 +30,25 @@ import {
ExecutorSubActionCommonFieldsParams,
ServiceNowExecutorResultData,
ExecutorSubActionGetChoicesParams,
+ ServiceFactory,
+ ExternalServiceAPI,
} from './types';
+import {
+ ServiceNowITSMActionTypeId,
+ serviceNowITSMTable,
+ ServiceNowSIRActionTypeId,
+ serviceNowSIRTable,
+ snExternalServiceConfig,
+} from './config';
+import { createExternalServiceSIR } from './service_sir';
+import { apiSIR } from './api_sir';
+
+export {
+ ServiceNowITSMActionTypeId,
+ serviceNowITSMTable,
+ ServiceNowSIRActionTypeId,
+ serviceNowSIRTable,
+};
export type ActionParamsType =
| TypeOf
@@ -41,12 +59,6 @@ interface GetActionTypeParams {
configurationUtilities: ActionsConfigurationUtilities;
}
-const serviceNowITSMTable = 'incident';
-const serviceNowSIRTable = 'sn_si_incident';
-
-export const ServiceNowITSMActionTypeId = '.servicenow';
-export const ServiceNowSIRActionTypeId = '.servicenow-sir';
-
export type ServiceNowActionType = ActionType<
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
@@ -79,8 +91,9 @@ export function getServiceNowITSMActionType(params: GetActionTypeParams): Servic
executor: curry(executor)({
logger,
configurationUtilities,
- table: serviceNowITSMTable,
- commentFieldKey: 'work_notes',
+ actionTypeId: ServiceNowITSMActionTypeId,
+ createService: createExternalService,
+ api: commonAPI,
}),
};
}
@@ -103,8 +116,9 @@ export function getServiceNowSIRActionType(params: GetActionTypeParams): Service
executor: curry(executor)({
logger,
configurationUtilities,
- table: serviceNowSIRTable,
- commentFieldKey: 'work_notes',
+ actionTypeId: ServiceNowSIRActionTypeId,
+ createService: createExternalServiceSIR,
+ api: apiSIR,
}),
};
}
@@ -115,28 +129,31 @@ async function executor(
{
logger,
configurationUtilities,
- table,
- commentFieldKey = 'comments',
+ actionTypeId,
+ createService,
+ api,
}: {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
- table: string;
- commentFieldKey?: string;
+ actionTypeId: string;
+ createService: ServiceFactory;
+ api: ExternalServiceAPI;
},
execOptions: ServiceNowActionTypeExecutorOptions
): Promise> {
const { actionId, config, params, secrets } = execOptions;
const { subAction, subActionParams } = params;
+ const externalServiceConfig = snExternalServiceConfig[actionTypeId];
let data: ServiceNowExecutorResultData | null = null;
- const externalService = createExternalService(
- table,
+ const externalService = createService(
{
config,
secrets,
},
logger,
- configurationUtilities
+ configurationUtilities,
+ externalServiceConfig
);
if (!api[subAction]) {
@@ -156,9 +173,10 @@ async function executor(
data = await api.pushToService({
externalService,
params: pushToServiceParams,
+ config,
secrets,
logger,
- commentFieldKey,
+ commentFieldKey: externalServiceConfig.commentFieldKey,
});
logger.debug(`response push to service for incident id: ${data.id}`);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
index 909200472be33..3629fb33915ae 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
@@ -5,7 +5,14 @@
* 2.0.
*/
-import { ExternalService, ExecutorSubActionPushParams } from './types';
+import {
+ ExternalService,
+ ExecutorSubActionPushParams,
+ PushToServiceApiParamsSIR,
+ ExternalServiceSIR,
+ Observable,
+ ObservableTypes,
+} from './types';
export const serviceNowCommonFields = [
{
@@ -74,6 +81,10 @@ const createMock = (): jest.Mocked => {
getFields: jest.fn().mockImplementation(() => Promise.resolve(serviceNowCommonFields)),
getIncident: jest.fn().mockImplementation(() =>
Promise.resolve({
+ id: 'incident-1',
+ title: 'INC01',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123',
short_description: 'title from servicenow',
description: 'description from servicenow',
})
@@ -95,16 +106,60 @@ const createMock = (): jest.Mocked => {
})
),
findIncidents: jest.fn(),
+ getApplicationInformation: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ name: 'Elastic',
+ scope: 'x_elas2_inc_int',
+ version: '1.0.0',
+ })
+ ),
+ checkIfApplicationIsInstalled: jest.fn(),
+ getUrl: jest.fn().mockImplementation(() => 'https://instance.service-now.com'),
+ checkInstance: jest.fn(),
};
return service;
};
-const externalServiceMock = {
+const createSIRMock = (): jest.Mocked => {
+ const service = {
+ ...createMock(),
+ addObservableToIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ value: 'https://example.com',
+ observable_sys_id: '3',
+ })
+ ),
+ bulkAddObservableToIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve([
+ {
+ value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9',
+ observable_sys_id: '1',
+ },
+ {
+ value: '127.0.0.1',
+ observable_sys_id: '2',
+ },
+ {
+ value: 'https://example.com',
+ observable_sys_id: '3',
+ },
+ ])
+ ),
+ };
+
+ return service;
+};
+
+export const externalServiceMock = {
create: createMock,
};
-const executorParams: ExecutorSubActionPushParams = {
+export const externalServiceSIRMock = {
+ create: createSIRMock,
+};
+
+export const executorParams: ExecutorSubActionPushParams = {
incident: {
externalId: 'incident-3',
short_description: 'Incident title',
@@ -114,6 +169,8 @@ const executorParams: ExecutorSubActionPushParams = {
impact: '3',
category: 'software',
subcategory: 'os',
+ correlation_id: 'ruleId',
+ correlation_display: 'Alerting',
},
comments: [
{
@@ -127,6 +184,46 @@ const executorParams: ExecutorSubActionPushParams = {
],
};
-const apiParams = executorParams;
+export const sirParams: PushToServiceApiParamsSIR = {
+ incident: {
+ externalId: 'incident-3',
+ short_description: 'Incident title',
+ description: 'Incident description',
+ dest_ip: ['192.168.1.1', '192.168.1.3'],
+ source_ip: ['192.168.1.2', '192.168.1.4'],
+ malware_hash: ['5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9'],
+ malware_url: ['https://example.com'],
+ category: 'software',
+ subcategory: 'os',
+ correlation_id: 'ruleId',
+ correlation_display: 'Alerting',
+ priority: '1',
+ },
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ },
+ {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ },
+ ],
+};
+
+export const observables: Observable[] = [
+ {
+ value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9',
+ type: ObservableTypes.sha256,
+ },
+ {
+ value: '127.0.0.1',
+ type: ObservableTypes.ip4,
+ },
+ {
+ value: 'https://example.com',
+ type: ObservableTypes.url,
+ },
+];
-export { externalServiceMock, executorParams, apiParams };
+export const apiParams = executorParams;
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
index 6fec30803d6d7..dab68bb9d3e9d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
@@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema';
export const ExternalIncidentServiceConfiguration = {
apiUrl: schema.string(),
+ isLegacy: schema.boolean({ defaultValue: false }),
};
export const ExternalIncidentServiceConfigurationSchema = schema.object(
@@ -39,6 +40,8 @@ const CommonAttributes = {
externalId: schema.nullable(schema.string()),
category: schema.nullable(schema.string()),
subcategory: schema.nullable(schema.string()),
+ correlation_id: schema.nullable(schema.string()),
+ correlation_display: schema.nullable(schema.string()),
};
// Schema for ServiceNow Incident Management (ITSM)
@@ -56,10 +59,22 @@ export const ExecutorSubActionPushParamsSchemaITSM = schema.object({
export const ExecutorSubActionPushParamsSchemaSIR = schema.object({
incident: schema.object({
...CommonAttributes,
- dest_ip: schema.nullable(schema.string()),
- malware_hash: schema.nullable(schema.string()),
- malware_url: schema.nullable(schema.string()),
- source_ip: schema.nullable(schema.string()),
+ dest_ip: schema.oneOf(
+ [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))],
+ { defaultValue: null }
+ ),
+ malware_hash: schema.oneOf(
+ [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))],
+ { defaultValue: null }
+ ),
+ malware_url: schema.oneOf(
+ [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))],
+ { defaultValue: null }
+ ),
+ source_ip: schema.oneOf(
+ [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))],
+ { defaultValue: null }
+ ),
priority: schema.nullable(schema.string()),
}),
comments: CommentsSchema,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts
index 37bfb662508a2..b8499b01e6a02 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts
@@ -5,15 +5,16 @@
* 2.0.
*/
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
import { createExternalService } from './service';
import * as utils from '../lib/axios_utils';
-import { ExternalService } from './types';
+import { ExternalService, ServiceNowITSMIncident } from './types';
import { Logger } from '../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../../actions_config.mock';
import { serviceNowCommonFields, serviceNowChoices } from './mocks';
+import { snExternalServiceConfig } from './config';
const logger = loggingSystemMock.create().get() as jest.Mocked;
jest.mock('axios');
@@ -28,24 +29,134 @@ jest.mock('../lib/axios_utils', () => {
axios.create = jest.fn(() => axios);
const requestMock = utils.request as jest.Mock;
-const patchMock = utils.patch as jest.Mock;
const configurationUtilities = actionsConfigMock.create();
-const table = 'incident';
+
+const getImportSetAPIResponse = (update = false) => ({
+ import_set: 'ISET01',
+ staging_table: 'x_elas2_inc_int_elastic_incident',
+ result: [
+ {
+ transform_map: 'Elastic Incident',
+ table: 'incident',
+ display_name: 'number',
+ display_value: 'INC01',
+ record_link: 'https://example.com/api/now/table/incident/1',
+ status: update ? 'updated' : 'inserted',
+ sys_id: '1',
+ },
+ ],
+});
+
+const getImportSetAPIError = () => ({
+ import_set: 'ISET01',
+ staging_table: 'x_elas2_inc_int_elastic_incident',
+ result: [
+ {
+ transform_map: 'Elastic Incident',
+ status: 'error',
+ error_message: 'An error has occurred while importing the incident',
+ status_message: 'failure',
+ },
+ ],
+});
+
+const mockApplicationVersion = () =>
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ result: { name: 'Elastic', scope: 'x_elas2_inc_int', version: '1.0.0' },
+ },
+ }));
+
+const mockImportIncident = (update: boolean) =>
+ requestMock.mockImplementationOnce(() => ({
+ data: getImportSetAPIResponse(update),
+ }));
+
+const mockIncidentResponse = (update: boolean) =>
+ requestMock.mockImplementation(() => ({
+ data: {
+ result: {
+ sys_id: '1',
+ number: 'INC01',
+ ...(update
+ ? { sys_updated_on: '2020-03-10 12:24:20' }
+ : { sys_created_on: '2020-03-10 12:24:20' }),
+ },
+ },
+ }));
+
+const createIncident = async (service: ExternalService) => {
+ // Get application version
+ mockApplicationVersion();
+ // Import set api response
+ mockImportIncident(false);
+ // Get incident response
+ mockIncidentResponse(false);
+
+ return await service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+};
+
+const updateIncident = async (service: ExternalService) => {
+ // Get application version
+ mockApplicationVersion();
+ // Import set api response
+ mockImportIncident(true);
+ // Get incident response
+ mockIncidentResponse(true);
+
+ return await service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+};
+
+const expectImportedIncident = (update: boolean) => {
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
+ method: 'get',
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(2, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident',
+ method: 'post',
+ data: {
+ u_short_description: 'title',
+ u_description: 'desc',
+ ...(update ? { elastic_incident_id: '1' } : {}),
+ },
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(3, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/incident/1',
+ method: 'get',
+ });
+};
describe('ServiceNow service', () => {
let service: ExternalService;
beforeEach(() => {
service = createExternalService(
- table,
{
// The trailing slash at the end of the url is intended.
// All API calls need to have the trailing slash removed.
- config: { apiUrl: 'https://dev102283.service-now.com/' },
+ config: { apiUrl: 'https://example.com/' },
secrets: { username: 'admin', password: 'admin' },
},
logger,
- configurationUtilities
+ configurationUtilities,
+ snExternalServiceConfig['.servicenow']
);
});
@@ -57,13 +168,13 @@ describe('ServiceNow service', () => {
test('throws without url', () => {
expect(() =>
createExternalService(
- table,
{
config: { apiUrl: null },
secrets: { username: 'admin', password: 'admin' },
},
logger,
- configurationUtilities
+ configurationUtilities,
+ snExternalServiceConfig['.servicenow']
)
).toThrow();
});
@@ -71,13 +182,13 @@ describe('ServiceNow service', () => {
test('throws without username', () => {
expect(() =>
createExternalService(
- table,
{
config: { apiUrl: 'test.com' },
secrets: { username: '', password: 'admin' },
},
logger,
- configurationUtilities
+ configurationUtilities,
+ snExternalServiceConfig['.servicenow']
)
).toThrow();
});
@@ -85,13 +196,13 @@ describe('ServiceNow service', () => {
test('throws without password', () => {
expect(() =>
createExternalService(
- table,
{
config: { apiUrl: 'test.com' },
secrets: { username: '', password: undefined },
},
logger,
- configurationUtilities
+ configurationUtilities,
+ snExternalServiceConfig['.servicenow']
)
).toThrow();
});
@@ -116,19 +227,20 @@ describe('ServiceNow service', () => {
axios,
logger,
configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1',
+ url: 'https://example.com/api/now/v2/table/incident/1',
+ method: 'get',
});
});
test('it should call request with correct arguments when table changes', async () => {
service = createExternalService(
- 'sn_si_incident',
{
- config: { apiUrl: 'https://dev102283.service-now.com/' },
+ config: { apiUrl: 'https://example.com/' },
secrets: { username: 'admin', password: 'admin' },
},
logger,
- configurationUtilities
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' }
);
requestMock.mockImplementation(() => ({
@@ -140,7 +252,8 @@ describe('ServiceNow service', () => {
axios,
logger,
configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident/1',
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'get',
});
});
@@ -166,214 +279,346 @@ describe('ServiceNow service', () => {
});
describe('createIncident', () => {
- test('it creates the incident correctly', async () => {
- requestMock.mockImplementation(() => ({
- data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } },
- }));
-
- const res = await service.createIncident({
- incident: { short_description: 'title', description: 'desc' },
+ // new connectors
+ describe('import set table', () => {
+ test('it creates the incident correctly', async () => {
+ const res = await createIncident(service);
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
});
- expect(res).toEqual({
- title: 'INC01',
- id: '1',
- pushedDate: '2020-03-10T12:24:20.000Z',
- url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1',
+ test('it should call request with correct arguments', async () => {
+ await createIncident(service);
+ expect(requestMock).toHaveBeenCalledTimes(3);
+ expectImportedIncident(false);
});
- });
- test('it should call request with correct arguments', async () => {
- requestMock.mockImplementation(() => ({
- data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } },
- }));
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ snExternalServiceConfig['.servicenow-sir']
+ );
- await service.createIncident({
- incident: { short_description: 'title', description: 'desc' },
- });
+ const res = await createIncident(service);
- expect(requestMock).toHaveBeenCalledWith({
- axios,
- logger,
- configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/incident',
- method: 'post',
- data: { short_description: 'title', description: 'desc' },
- });
- });
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
+ method: 'get',
+ });
- test('it should call request with correct arguments when table changes', async () => {
- service = createExternalService(
- 'sn_si_incident',
- {
- config: { apiUrl: 'https://dev102283.service-now.com/' },
- secrets: { username: 'admin', password: 'admin' },
- },
- logger,
- configurationUtilities
- );
+ expect(requestMock).toHaveBeenNthCalledWith(2, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
+ method: 'post',
+ data: { u_short_description: 'title', u_description: 'desc' },
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(3, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'get',
+ });
- requestMock.mockImplementation(() => ({
- data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } },
- }));
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
+ });
- const res = await service.createIncident({
- incident: { short_description: 'title', description: 'desc' },
+ test('it should throw an error when the application is not installed', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(
+ service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to create incident. Error: [Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown: errorResponse was null Reason: unknown: errorResponse was null'
+ );
});
- expect(requestMock).toHaveBeenCalledWith({
- axios,
- logger,
- configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident',
- method: 'post',
- data: { short_description: 'title', description: 'desc' },
+ test('it should throw an error when instance is not alive', async () => {
+ requestMock.mockImplementation(() => ({
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'Developer instance' } },
+ }));
+ await expect(
+ service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ 'There is an issue with your Service Now Instance. Please check Developer instance.'
+ );
});
- expect(res.url).toEqual(
- 'https://dev102283.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'
- );
+ test('it should throw an error when there is an import set api error', async () => {
+ requestMock.mockImplementation(() => ({ data: getImportSetAPIError() }));
+ await expect(
+ service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred while importing the incident Reason: unknown'
+ );
+ });
});
- test('it should throw an error', async () => {
- requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ // old connectors
+ describe('table API', () => {
+ beforeEach(() => {
+ service = createExternalService(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow'], useImportAPI: false }
+ );
});
- await expect(
- service.createIncident({
- incident: { short_description: 'title', description: 'desc' },
- })
- ).rejects.toThrow(
- '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred'
- );
- });
+ test('it creates the incident correctly', async () => {
+ mockIncidentResponse(false);
+ const res = await service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
+
+ expect(requestMock).toHaveBeenCalledTimes(2);
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/incident',
+ method: 'post',
+ data: { short_description: 'title', description: 'desc' },
+ });
+ });
- test('it should throw an error when instance is not alive', async () => {
- requestMock.mockImplementation(() => ({
- status: 200,
- data: {},
- request: { connection: { servername: 'Developer instance' } },
- }));
- await expect(service.getIncident('1')).rejects.toThrow(
- 'There is an issue with your Service Now Instance. Please check Developer instance.'
- );
- });
- });
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false }
+ );
- describe('updateIncident', () => {
- test('it updates the incident correctly', async () => {
- patchMock.mockImplementation(() => ({
- data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } },
- }));
+ mockIncidentResponse(false);
- const res = await service.updateIncident({
- incidentId: '1',
- incident: { short_description: 'title', description: 'desc' },
- });
+ const res = await service.createIncident({
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident',
+ method: 'post',
+ data: { short_description: 'title', description: 'desc' },
+ });
- expect(res).toEqual({
- title: 'INC01',
- id: '1',
- pushedDate: '2020-03-10T12:24:20.000Z',
- url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1',
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
});
});
+ });
- test('it should call request with correct arguments', async () => {
- patchMock.mockImplementation(() => ({
- data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } },
- }));
-
- await service.updateIncident({
- incidentId: '1',
- incident: { short_description: 'title', description: 'desc' },
+ describe('updateIncident', () => {
+ // new connectors
+ describe('import set table', () => {
+ test('it updates the incident correctly', async () => {
+ const res = await updateIncident(service);
+
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
});
- expect(patchMock).toHaveBeenCalledWith({
- axios,
- logger,
- configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1',
- data: { short_description: 'title', description: 'desc' },
+ test('it should call request with correct arguments', async () => {
+ await updateIncident(service);
+ expectImportedIncident(true);
});
- });
- test('it should call request with correct arguments when table changes', async () => {
- service = createExternalService(
- 'sn_si_incident',
- {
- config: { apiUrl: 'https://dev102283.service-now.com/' },
- secrets: { username: 'admin', password: 'admin' },
- },
- logger,
- configurationUtilities
- );
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ snExternalServiceConfig['.servicenow-sir']
+ );
- patchMock.mockImplementation(() => ({
- data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } },
- }));
+ const res = await updateIncident(service);
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
+ method: 'get',
+ });
- const res = await service.updateIncident({
- incidentId: '1',
- incident: { short_description: 'title', description: 'desc' },
+ expect(requestMock).toHaveBeenNthCalledWith(2, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
+ method: 'post',
+ data: { u_short_description: 'title', u_description: 'desc', elastic_incident_id: '1' },
+ });
+
+ expect(requestMock).toHaveBeenNthCalledWith(3, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'get',
+ });
+
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
});
- expect(patchMock).toHaveBeenCalledWith({
- axios,
- logger,
- configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident/1',
- data: { short_description: 'title', description: 'desc' },
+ test('it should throw an error when the application is not installed', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ await expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to update incident with id 1. Error: [Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown: errorResponse was null Reason: unknown: errorResponse was null'
+ );
});
- expect(res.url).toEqual(
- 'https://dev102283.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'
- );
+ test('it should throw an error when instance is not alive', async () => {
+ requestMock.mockImplementation(() => ({
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'Developer instance' } },
+ }));
+ await expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ 'There is an issue with your Service Now Instance. Please check Developer instance.'
+ );
+ });
+
+ test('it should throw an error when there is an import set api error', async () => {
+ requestMock.mockImplementation(() => ({ data: getImportSetAPIError() }));
+ await expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ })
+ ).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred while importing the incident Reason: unknown'
+ );
+ });
});
- test('it should throw an error', async () => {
- patchMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ // old connectors
+ describe('table API', () => {
+ beforeEach(() => {
+ service = createExternalService(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow'], useImportAPI: false }
+ );
});
- await expect(
- service.updateIncident({
+ test('it updates the incident correctly', async () => {
+ mockIncidentResponse(true);
+ const res = await service.updateIncident({
incidentId: '1',
- incident: { short_description: 'title', description: 'desc' },
- })
- ).rejects.toThrow(
- '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred'
- );
- });
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
+
+ expect(res).toEqual({
+ title: 'INC01',
+ id: '1',
+ pushedDate: '2020-03-10T12:24:20.000Z',
+ url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1',
+ });
+
+ expect(requestMock).toHaveBeenCalledTimes(2);
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/incident/1',
+ method: 'patch',
+ data: { short_description: 'title', description: 'desc' },
+ });
+ });
- test('it creates the comment correctly', async () => {
- patchMock.mockImplementation(() => ({
- data: { result: { sys_id: '11', number: 'INC011', sys_updated_on: '2020-03-10 12:24:20' } },
- }));
+ test('it should call request with correct arguments when table changes', async () => {
+ service = createExternalService(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false }
+ );
- const res = await service.updateIncident({
- incidentId: '1',
- comment: 'comment-1',
- });
+ mockIncidentResponse(false);
- expect(res).toEqual({
- title: 'INC011',
- id: '11',
- pushedDate: '2020-03-10T12:24:20.000Z',
- url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=11',
- });
- });
+ const res = await service.updateIncident({
+ incidentId: '1',
+ incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident,
+ });
- test('it should throw an error when instance is not alive', async () => {
- requestMock.mockImplementation(() => ({
- status: 200,
- data: {},
- request: { connection: { servername: 'Developer instance' } },
- }));
- await expect(service.getIncident('1')).rejects.toThrow(
- 'There is an issue with your Service Now Instance. Please check Developer instance.'
- );
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
+ method: 'patch',
+ data: { short_description: 'title', description: 'desc' },
+ });
+
+ expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
+ });
});
});
@@ -388,7 +633,7 @@ describe('ServiceNow service', () => {
axios,
logger,
configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
+ url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
});
});
@@ -402,13 +647,13 @@ describe('ServiceNow service', () => {
test('it should call request with correct arguments when table changes', async () => {
service = createExternalService(
- 'sn_si_incident',
{
- config: { apiUrl: 'https://dev102283.service-now.com/' },
+ config: { apiUrl: 'https://example.com/' },
secrets: { username: 'admin', password: 'admin' },
},
logger,
- configurationUtilities
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' }
);
requestMock.mockImplementation(() => ({
@@ -420,7 +665,7 @@ describe('ServiceNow service', () => {
axios,
logger,
configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
+ url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
});
});
@@ -456,7 +701,7 @@ describe('ServiceNow service', () => {
axios,
logger,
configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element',
+ url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element',
});
});
@@ -470,13 +715,13 @@ describe('ServiceNow service', () => {
test('it should call request with correct arguments when table changes', async () => {
service = createExternalService(
- 'sn_si_incident',
{
- config: { apiUrl: 'https://dev102283.service-now.com/' },
+ config: { apiUrl: 'https://example.com/' },
secrets: { username: 'admin', password: 'admin' },
},
logger,
- configurationUtilities
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' }
);
requestMock.mockImplementation(() => ({
@@ -489,7 +734,7 @@ describe('ServiceNow service', () => {
axios,
logger,
configurationUtilities,
- url: 'https://dev102283.service-now.com/api/now/v2/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element',
+ url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element',
});
});
@@ -513,4 +758,79 @@ describe('ServiceNow service', () => {
);
});
});
+
+ describe('getUrl', () => {
+ test('it returns the instance url', async () => {
+ expect(service.getUrl()).toBe('https://example.com');
+ });
+ });
+
+ describe('checkInstance', () => {
+ test('it throws an error if there is no result on data', () => {
+ const res = { status: 200, data: {} } as AxiosResponse;
+ expect(() => service.checkInstance(res)).toThrow();
+ });
+
+ test('it does NOT throws an error if the status > 400', () => {
+ const res = { status: 500, data: {} } as AxiosResponse;
+ expect(() => service.checkInstance(res)).not.toThrow();
+ });
+
+ test('it shows the servername', () => {
+ const res = {
+ status: 200,
+ data: {},
+ request: { connection: { servername: 'https://example.com' } },
+ } as AxiosResponse;
+ expect(() => service.checkInstance(res)).toThrow(
+ 'There is an issue with your Service Now Instance. Please check https://example.com.'
+ );
+ });
+
+ describe('getApplicationInformation', () => {
+ test('it returns the application information', async () => {
+ mockApplicationVersion();
+ const res = await service.getApplicationInformation();
+ expect(res).toEqual({
+ name: 'Elastic',
+ scope: 'x_elas2_inc_int',
+ version: '1.0.0',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+ await expect(service.getApplicationInformation()).rejects.toThrow(
+ '[Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown'
+ );
+ });
+ });
+
+ describe('checkIfApplicationIsInstalled', () => {
+ test('it logs the application information', async () => {
+ mockApplicationVersion();
+ await service.checkIfApplicationIsInstalled();
+ expect(logger.debug).toHaveBeenCalledWith(
+ 'Create incident: Application scope: x_elas2_inc_int: Application version1.0.0'
+ );
+ });
+
+ test('it does not log if useOldApi = true', async () => {
+ service = createExternalService(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ { ...snExternalServiceConfig['.servicenow'], useImportAPI: false }
+ );
+ await service.checkIfApplicationIsInstalled();
+ expect(requestMock).not.toHaveBeenCalled();
+ expect(logger.debug).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts
index 07ed9edc94d39..cb030c7bb6933 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts
@@ -7,28 +7,35 @@
import axios, { AxiosResponse } from 'axios';
-import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types';
+import {
+ ExternalServiceCredentials,
+ ExternalService,
+ ExternalServiceParamsCreate,
+ ExternalServiceParamsUpdate,
+ ImportSetApiResponse,
+ ImportSetApiResponseError,
+ ServiceNowIncident,
+ GetApplicationInfoResponse,
+ SNProductsConfigValue,
+ ServiceFactory,
+} from './types';
import * as i18n from './translations';
import { Logger } from '../../../../../../src/core/server';
-import {
- ServiceNowPublicConfigurationType,
- ServiceNowSecretConfigurationType,
- ResponseError,
-} from './types';
-import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils';
+import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types';
+import { request } from '../lib/axios_utils';
import { ActionsConfigurationUtilities } from '../../actions_config';
+import { createServiceError, getPushedDate, prepareIncident } from './utils';
-const API_VERSION = 'v2';
-const SYS_DICTIONARY = `api/now/${API_VERSION}/table/sys_dictionary`;
+export const SYS_DICTIONARY_ENDPOINT = `api/now/table/sys_dictionary`;
-export const createExternalService = (
- table: string,
+export const createExternalService: ServiceFactory = (
{ config, secrets }: ExternalServiceCredentials,
logger: Logger,
- configurationUtilities: ActionsConfigurationUtilities
+ configurationUtilities: ActionsConfigurationUtilities,
+ { table, importSetTable, useImportAPI, appScope }: SNProductsConfigValue
): ExternalService => {
- const { apiUrl: url } = config as ServiceNowPublicConfigurationType;
+ const { apiUrl: url, isLegacy } = config as ServiceNowPublicConfigurationType;
const { username, password } = secrets as ServiceNowSecretConfigurationType;
if (!url || !username || !password) {
@@ -36,13 +43,26 @@ export const createExternalService = (
}
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
- const incidentUrl = `${urlWithoutTrailingSlash}/api/now/${API_VERSION}/table/${table}`;
- const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^ORname=${table}^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`;
- const choicesUrl = `${urlWithoutTrailingSlash}/api/now/${API_VERSION}/table/sys_choice`;
+ const importSetTableUrl = `${urlWithoutTrailingSlash}/api/now/import/${importSetTable}`;
+ const tableApiIncidentUrl = `${urlWithoutTrailingSlash}/api/now/v2/table/${table}`;
+ const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY_ENDPOINT}?sysparm_query=name=task^ORname=${table}^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`;
+ const choicesUrl = `${urlWithoutTrailingSlash}/api/now/table/sys_choice`;
+ /**
+ * Need to be set the same at:
+ * x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts
+ */
+ const getVersionUrl = () => `${urlWithoutTrailingSlash}/api/${appScope}/elastic_api/health`;
+
const axiosInstance = axios.create({
auth: { username, password },
});
+ const useOldApi = !useImportAPI || isLegacy;
+
+ const getCreateIncidentUrl = () => (useOldApi ? tableApiIncidentUrl : importSetTableUrl);
+ const getUpdateIncidentUrl = (incidentId: string) =>
+ useOldApi ? `${tableApiIncidentUrl}/${incidentId}` : importSetTableUrl;
+
const getIncidentViewURL = (id: string) => {
// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html
return `${urlWithoutTrailingSlash}/nav_to.do?uri=${table}.do?sys_id=${id}`;
@@ -57,7 +77,7 @@ export const createExternalService = (
};
const checkInstance = (res: AxiosResponse) => {
- if (res.status === 200 && res.data.result == null) {
+ if (res.status >= 200 && res.status < 400 && res.data.result == null) {
throw new Error(
`There is an issue with your Service Now Instance. Please check ${
res.request?.connection?.servername ?? ''
@@ -66,34 +86,70 @@ export const createExternalService = (
}
};
- const createErrorMessage = (errorResponse: ResponseError): string => {
- if (errorResponse == null) {
- return '';
+ const isImportSetApiResponseAnError = (
+ data: ImportSetApiResponse['result'][0]
+ ): data is ImportSetApiResponseError['result'][0] => data.status === 'error';
+
+ const throwIfImportSetApiResponseIsAnError = (res: ImportSetApiResponse) => {
+ if (res.result.length === 0) {
+ throw new Error('Unexpected result');
}
- const { error } = errorResponse;
- return error != null ? `${error?.message}: ${error?.detail}` : '';
+ const data = res.result[0];
+
+ // Create ResponseError message?
+ if (isImportSetApiResponseAnError(data)) {
+ throw new Error(data.error_message);
+ }
};
- const getIncident = async (id: string) => {
+ /**
+ * Gets the Elastic SN Application information including the current version.
+ * It should not be used on legacy connectors.
+ */
+ const getApplicationInformation = async (): Promise => {
try {
const res = await request({
axios: axiosInstance,
- url: `${incidentUrl}/${id}`,
+ url: getVersionUrl(),
logger,
configurationUtilities,
+ method: 'get',
});
+
checkInstance(res);
+
return { ...res.data.result };
} catch (error) {
- throw new Error(
- getErrorMessage(
- i18n.SERVICENOW,
- `Unable to get incident with id ${id}. Error: ${
- error.message
- } Reason: ${createErrorMessage(error.response?.data)}`
- )
- );
+ throw createServiceError(error, 'Unable to get application version');
+ }
+ };
+
+ const logApplicationInfo = (scope: string, version: string) =>
+ logger.debug(`Create incident: Application scope: ${scope}: Application version${version}`);
+
+ const checkIfApplicationIsInstalled = async () => {
+ if (!useOldApi) {
+ const { version, scope } = await getApplicationInformation();
+ logApplicationInfo(scope, version);
+ }
+ };
+
+ const getIncident = async (id: string): Promise => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ url: `${tableApiIncidentUrl}/${id}`,
+ logger,
+ configurationUtilities,
+ method: 'get',
+ });
+
+ checkInstance(res);
+
+ return { ...res.data.result };
+ } catch (error) {
+ throw createServiceError(error, `Unable to get incident with id ${id}`);
}
};
@@ -101,7 +157,7 @@ export const createExternalService = (
try {
const res = await request({
axios: axiosInstance,
- url: incidentUrl,
+ url: tableApiIncidentUrl,
logger,
params,
configurationUtilities,
@@ -109,71 +165,80 @@ export const createExternalService = (
checkInstance(res);
return res.data.result.length > 0 ? { ...res.data.result } : undefined;
} catch (error) {
- throw new Error(
- getErrorMessage(
- i18n.SERVICENOW,
- `Unable to find incidents by query. Error: ${error.message} Reason: ${createErrorMessage(
- error.response?.data
- )}`
- )
- );
+ throw createServiceError(error, 'Unable to find incidents by query');
}
};
- const createIncident = async ({ incident }: ExternalServiceParams) => {
+ const getUrl = () => urlWithoutTrailingSlash;
+
+ const createIncident = async ({ incident }: ExternalServiceParamsCreate) => {
try {
+ await checkIfApplicationIsInstalled();
+
const res = await request({
axios: axiosInstance,
- url: `${incidentUrl}`,
+ url: getCreateIncidentUrl(),
logger,
method: 'post',
- data: { ...(incident as Record) },
+ data: prepareIncident(useOldApi, incident),
configurationUtilities,
});
+
checkInstance(res);
+
+ if (!useOldApi) {
+ throwIfImportSetApiResponseIsAnError(res.data);
+ }
+
+ const incidentId = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id;
+ const insertedIncident = await getIncident(incidentId);
+
return {
- title: res.data.result.number,
- id: res.data.result.sys_id,
- pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(),
- url: getIncidentViewURL(res.data.result.sys_id),
+ title: insertedIncident.number,
+ id: insertedIncident.sys_id,
+ pushedDate: getPushedDate(insertedIncident.sys_created_on),
+ url: getIncidentViewURL(insertedIncident.sys_id),
};
} catch (error) {
- throw new Error(
- getErrorMessage(
- i18n.SERVICENOW,
- `Unable to create incident. Error: ${error.message} Reason: ${createErrorMessage(
- error.response?.data
- )}`
- )
- );
+ throw createServiceError(error, 'Unable to create incident');
}
};
- const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {
+ const updateIncident = async ({ incidentId, incident }: ExternalServiceParamsUpdate) => {
try {
- const res = await patch({
+ await checkIfApplicationIsInstalled();
+
+ const res = await request({
axios: axiosInstance,
- url: `${incidentUrl}/${incidentId}`,
+ url: getUpdateIncidentUrl(incidentId),
+ // Import Set API supports only POST.
+ method: useOldApi ? 'patch' : 'post',
logger,
- data: { ...(incident as Record) },
+ data: {
+ ...prepareIncident(useOldApi, incident),
+ // elastic_incident_id is used to update the incident when using the Import Set API.
+ ...(useOldApi ? {} : { elastic_incident_id: incidentId }),
+ },
configurationUtilities,
});
+
checkInstance(res);
+
+ if (!useOldApi) {
+ throwIfImportSetApiResponseIsAnError(res.data);
+ }
+
+ const id = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id;
+ const updatedIncident = await getIncident(id);
+
return {
- title: res.data.result.number,
- id: res.data.result.sys_id,
- pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(),
- url: getIncidentViewURL(res.data.result.sys_id),
+ title: updatedIncident.number,
+ id: updatedIncident.sys_id,
+ pushedDate: getPushedDate(updatedIncident.sys_updated_on),
+ url: getIncidentViewURL(updatedIncident.sys_id),
};
} catch (error) {
- throw new Error(
- getErrorMessage(
- i18n.SERVICENOW,
- `Unable to update incident with id ${incidentId}. Error: ${
- error.message
- } Reason: ${createErrorMessage(error.response?.data)}`
- )
- );
+ throw createServiceError(error, `Unable to update incident with id ${incidentId}`);
}
};
@@ -185,17 +250,12 @@ export const createExternalService = (
logger,
configurationUtilities,
});
+
checkInstance(res);
+
return res.data.result.length > 0 ? res.data.result : [];
} catch (error) {
- throw new Error(
- getErrorMessage(
- i18n.SERVICENOW,
- `Unable to get fields. Error: ${error.message} Reason: ${createErrorMessage(
- error.response?.data
- )}`
- )
- );
+ throw createServiceError(error, 'Unable to get fields');
}
};
@@ -210,14 +270,7 @@ export const createExternalService = (
checkInstance(res);
return res.data.result;
} catch (error) {
- throw new Error(
- getErrorMessage(
- i18n.SERVICENOW,
- `Unable to get choices. Error: ${error.message} Reason: ${createErrorMessage(
- error.response?.data
- )}`
- )
- );
+ throw createServiceError(error, 'Unable to get choices');
}
};
@@ -228,5 +281,9 @@ export const createExternalService = (
getIncident,
updateIncident,
getChoices,
+ getUrl,
+ checkInstance,
+ getApplicationInformation,
+ checkIfApplicationIsInstalled,
};
};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.test.ts
new file mode 100644
index 0000000000000..0fc94b6287abd
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.test.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 axios from 'axios';
+
+import { createExternalServiceSIR } from './service_sir';
+import * as utils from '../lib/axios_utils';
+import { ExternalServiceSIR } from './types';
+import { Logger } from '../../../../../../src/core/server';
+import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
+import { actionsConfigMock } from '../../actions_config.mock';
+import { observables } from './mocks';
+import { snExternalServiceConfig } from './config';
+
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+
+jest.mock('axios');
+jest.mock('../lib/axios_utils', () => {
+ const originalUtils = jest.requireActual('../lib/axios_utils');
+ return {
+ ...originalUtils,
+ request: jest.fn(),
+ patch: jest.fn(),
+ };
+});
+
+axios.create = jest.fn(() => axios);
+const requestMock = utils.request as jest.Mock;
+const configurationUtilities = actionsConfigMock.create();
+
+const mockApplicationVersion = () =>
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ result: { name: 'Elastic', scope: 'x_elas2_sir_int', version: '1.0.0' },
+ },
+ }));
+
+const getAddObservablesResponse = () => [
+ {
+ value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9',
+ observable_sys_id: '1',
+ },
+ {
+ value: '127.0.0.1',
+ observable_sys_id: '2',
+ },
+ {
+ value: 'https://example.com',
+ observable_sys_id: '3',
+ },
+];
+
+const mockAddObservablesResponse = (single: boolean) => {
+ const res = getAddObservablesResponse();
+ requestMock.mockImplementation(() => ({
+ data: {
+ result: single ? res[0] : res,
+ },
+ }));
+};
+
+const expectAddObservables = (single: boolean) => {
+ expect(requestMock).toHaveBeenNthCalledWith(1, {
+ axios,
+ logger,
+ configurationUtilities,
+ url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
+ method: 'get',
+ });
+
+ const url = single
+ ? 'https://example.com/api/x_elas2_sir_int/elastic_api/incident/incident-1/observables'
+ : 'https://example.com/api/x_elas2_sir_int/elastic_api/incident/incident-1/observables/bulk';
+
+ const data = single ? observables[0] : observables;
+
+ expect(requestMock).toHaveBeenNthCalledWith(2, {
+ axios,
+ logger,
+ configurationUtilities,
+ url,
+ method: 'post',
+ data,
+ });
+};
+
+describe('ServiceNow SIR service', () => {
+ let service: ExternalServiceSIR;
+
+ beforeEach(() => {
+ service = createExternalServiceSIR(
+ {
+ config: { apiUrl: 'https://example.com/' },
+ secrets: { username: 'admin', password: 'admin' },
+ },
+ logger,
+ configurationUtilities,
+ snExternalServiceConfig['.servicenow-sir']
+ ) as ExternalServiceSIR;
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('bulkAddObservableToIncident', () => {
+ test('it adds multiple observables correctly', async () => {
+ mockApplicationVersion();
+ mockAddObservablesResponse(false);
+
+ const res = await service.bulkAddObservableToIncident(observables, 'incident-1');
+ expect(res).toEqual(getAddObservablesResponse());
+ expectAddObservables(false);
+ });
+
+ test('it adds a single observable correctly', async () => {
+ mockApplicationVersion();
+ mockAddObservablesResponse(true);
+
+ const res = await service.addObservableToIncident(observables[0], 'incident-1');
+ expect(res).toEqual(getAddObservablesResponse()[0]);
+ expectAddObservables(true);
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.ts
new file mode 100644
index 0000000000000..fc8d8cc555bc8
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.ts
@@ -0,0 +1,104 @@
+/*
+ * 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 axios from 'axios';
+
+import {
+ ExternalServiceCredentials,
+ SNProductsConfigValue,
+ Observable,
+ ExternalServiceSIR,
+ ObservableResponse,
+ ServiceFactory,
+} from './types';
+
+import { Logger } from '../../../../../../src/core/server';
+import { ServiceNowSecretConfigurationType } from './types';
+import { request } from '../lib/axios_utils';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { createExternalService } from './service';
+import { createServiceError } from './utils';
+
+const getAddObservableToIncidentURL = (url: string, incidentID: string) =>
+ `${url}/api/x_elas2_sir_int/elastic_api/incident/${incidentID}/observables`;
+
+const getBulkAddObservableToIncidentURL = (url: string, incidentID: string) =>
+ `${url}/api/x_elas2_sir_int/elastic_api/incident/${incidentID}/observables/bulk`;
+
+export const createExternalServiceSIR: ServiceFactory = (
+ credentials: ExternalServiceCredentials,
+ logger: Logger,
+ configurationUtilities: ActionsConfigurationUtilities,
+ serviceConfig: SNProductsConfigValue
+): ExternalServiceSIR => {
+ const snService = createExternalService(
+ credentials,
+ logger,
+ configurationUtilities,
+ serviceConfig
+ );
+
+ const { username, password } = credentials.secrets as ServiceNowSecretConfigurationType;
+ const axiosInstance = axios.create({
+ auth: { username, password },
+ });
+
+ const _addObservable = async (data: Observable | Observable[], url: string) => {
+ snService.checkIfApplicationIsInstalled();
+
+ const res = await request({
+ axios: axiosInstance,
+ url,
+ logger,
+ method: 'post',
+ data,
+ configurationUtilities,
+ });
+
+ snService.checkInstance(res);
+ return res.data.result;
+ };
+
+ const addObservableToIncident = async (
+ observable: Observable,
+ incidentID: string
+ ): Promise => {
+ try {
+ return await _addObservable(
+ observable,
+ getAddObservableToIncidentURL(snService.getUrl(), incidentID)
+ );
+ } catch (error) {
+ throw createServiceError(
+ error,
+ `Unable to add observable to security incident with id ${incidentID}`
+ );
+ }
+ };
+
+ const bulkAddObservableToIncident = async (
+ observables: Observable[],
+ incidentID: string
+ ): Promise => {
+ try {
+ return await _addObservable(
+ observables,
+ getBulkAddObservableToIncidentURL(snService.getUrl(), incidentID)
+ );
+ } catch (error) {
+ throw createServiceError(
+ error,
+ `Unable to add observables to security incident with id ${incidentID}`
+ );
+ }
+ };
+ return {
+ ...snService,
+ addObservableToIncident,
+ bulkAddObservableToIncident,
+ };
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
index 50631cf289a73..ecca1e55e0fec 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
@@ -7,6 +7,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+import { AxiosError, AxiosResponse } from 'axios';
import { TypeOf } from '@kbn/config-schema';
import {
ExecutorParamsSchemaITSM,
@@ -78,15 +79,29 @@ export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
comments?: ExternalServiceCommentResponse[];
}
-export type ExternalServiceParams = Record;
+export type Incident = ServiceNowITSMIncident | ServiceNowSIRIncident;
+export type PartialIncident = Partial;
+
+export interface ExternalServiceParamsCreate {
+ incident: Incident & Record;
+}
+
+export interface ExternalServiceParamsUpdate {
+ incidentId: string;
+ incident: PartialIncident & Record;
+}
export interface ExternalService {
getChoices: (fields: string[]) => Promise;
- getIncident: (id: string) => Promise;
+ getIncident: (id: string) => Promise;
getFields: () => Promise;
- createIncident: (params: ExternalServiceParams) => Promise;
- updateIncident: (params: ExternalServiceParams) => Promise;
- findIncidents: (params?: Record) => Promise;
+ createIncident: (params: ExternalServiceParamsCreate) => Promise;
+ updateIncident: (params: ExternalServiceParamsUpdate) => Promise;
+ findIncidents: (params?: Record) => Promise;
+ getUrl: () => string;
+ checkInstance: (res: AxiosResponse) => void;
+ getApplicationInformation: () => Promise;
+ checkIfApplicationIsInstalled: () => Promise;
}
export type PushToServiceApiParams = ExecutorSubActionPushParams;
@@ -115,10 +130,9 @@ export type ServiceNowSIRIncident = Omit<
'externalId'
>;
-export type Incident = ServiceNowITSMIncident | ServiceNowSIRIncident;
-
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
params: PushToServiceApiParams;
+ config: Record;
secrets: Record;
logger: Logger;
commentFieldKey: string;
@@ -158,12 +172,20 @@ export interface GetChoicesHandlerArgs {
params: ExecutorSubActionGetChoicesParams;
}
-export interface ExternalServiceApi {
+export interface ServiceNowIncident {
+ sys_id: string;
+ number: string;
+ sys_created_on: string;
+ sys_updated_on: string;
+ [x: string]: unknown;
+}
+
+export interface ExternalServiceAPI {
getChoices: (args: GetChoicesHandlerArgs) => Promise;
getFields: (args: GetCommonFieldsHandlerArgs) => Promise;
handshake: (args: HandshakeApiHandlerArgs) => Promise;
pushToService: (args: PushToServiceApiHandlerArgs) => Promise;
- getIncident: (args: GetIncidentApiHandlerArgs) => Promise;
+ getIncident: (args: GetIncidentApiHandlerArgs) => Promise;
}
export interface ExternalServiceCommentResponse {
@@ -173,10 +195,90 @@ export interface ExternalServiceCommentResponse {
}
type TypeNullOrUndefined = T | null | undefined;
-export interface ResponseError {
+
+export interface ServiceNowError {
error: TypeNullOrUndefined<{
message: TypeNullOrUndefined;
detail: TypeNullOrUndefined;
}>;
status: TypeNullOrUndefined;
}
+
+export type ResponseError = AxiosError;
+
+export interface ImportSetApiResponseSuccess {
+ import_set: string;
+ staging_table: string;
+ result: Array<{
+ display_name: string;
+ display_value: string;
+ record_link: string;
+ status: string;
+ sys_id: string;
+ table: string;
+ transform_map: string;
+ }>;
+}
+
+export interface ImportSetApiResponseError {
+ import_set: string;
+ staging_table: string;
+ result: Array<{
+ error_message: string;
+ status_message: string;
+ status: string;
+ transform_map: string;
+ }>;
+}
+
+export type ImportSetApiResponse = ImportSetApiResponseSuccess | ImportSetApiResponseError;
+export interface GetApplicationInfoResponse {
+ id: string;
+ name: string;
+ scope: string;
+ version: string;
+}
+
+export interface SNProductsConfigValue {
+ table: string;
+ appScope: string;
+ useImportAPI: boolean;
+ importSetTable: string;
+ commentFieldKey: string;
+}
+
+export type SNProductsConfig = Record;
+
+export enum ObservableTypes {
+ ip4 = 'ipv4-addr',
+ url = 'URL',
+ sha256 = 'SHA256',
+}
+
+export interface Observable {
+ value: string;
+ type: ObservableTypes;
+}
+
+export interface ObservableResponse {
+ value: string;
+ observable_sys_id: ObservableTypes;
+}
+
+export interface ExternalServiceSIR extends ExternalService {
+ addObservableToIncident: (
+ observable: Observable,
+ incidentID: string
+ ) => Promise;
+ bulkAddObservableToIncident: (
+ observables: Observable[],
+ incidentID: string
+ ) => Promise;
+}
+
+export type ServiceFactory = (
+ credentials: ExternalServiceCredentials,
+ logger: Logger,
+ configurationUtilities: ActionsConfigurationUtilities,
+ serviceConfig: SNProductsConfigValue
+) => ExternalServiceSIR | ExternalService;
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts
new file mode 100644
index 0000000000000..87f27da6d213f
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AxiosError } from 'axios';
+import { prepareIncident, createServiceError, getPushedDate } from './utils';
+
+/**
+ * The purpose of this test is to
+ * prevent developers from accidentally
+ * change important configuration values
+ * such as the scope or the import set table
+ * of our ServiceNow application
+ */
+
+describe('utils', () => {
+ describe('prepareIncident', () => {
+ test('it prepares the incident correctly when useOldApi=false', async () => {
+ const incident = { short_description: 'title', description: 'desc' };
+ const newIncident = prepareIncident(false, incident);
+ expect(newIncident).toEqual({ u_short_description: 'title', u_description: 'desc' });
+ });
+
+ test('it prepares the incident correctly when useOldApi=true', async () => {
+ const incident = { short_description: 'title', description: 'desc' };
+ const newIncident = prepareIncident(true, incident);
+ expect(newIncident).toEqual(incident);
+ });
+ });
+
+ describe('createServiceError', () => {
+ test('it creates an error when the response is null', async () => {
+ const error = new Error('An error occurred');
+ // @ts-expect-error
+ expect(createServiceError(error, 'Unable to do action').message).toBe(
+ '[Action][ServiceNow]: Unable to do action. Error: An error occurred Reason: unknown: errorResponse was null'
+ );
+ });
+
+ test('it creates an error with response correctly', async () => {
+ const axiosError = {
+ message: 'An error occurred',
+ response: { data: { error: { message: 'Denied', detail: 'no access' } } },
+ } as AxiosError;
+
+ expect(createServiceError(axiosError, 'Unable to do action').message).toBe(
+ '[Action][ServiceNow]: Unable to do action. Error: An error occurred Reason: Denied: no access'
+ );
+ });
+
+ test('it creates an error correctly when the ServiceNow error is null', async () => {
+ const axiosError = {
+ message: 'An error occurred',
+ response: { data: { error: null } },
+ } as AxiosError;
+
+ expect(createServiceError(axiosError, 'Unable to do action').message).toBe(
+ '[Action][ServiceNow]: Unable to do action. Error: An error occurred Reason: unknown: no error in error response'
+ );
+ });
+ });
+
+ describe('getPushedDate', () => {
+ beforeAll(() => {
+ jest.useFakeTimers('modern');
+ jest.setSystemTime(new Date('2021-10-04 11:15:06 GMT'));
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ test('it formats the date correctly if timestamp is provided', async () => {
+ expect(getPushedDate('2021-10-04 11:15:06')).toBe('2021-10-04T11:15:06.000Z');
+ });
+
+ test('it formats the date correctly if timestamp is not provided', async () => {
+ expect(getPushedDate()).toBe('2021-10-04T11:15:06.000Z');
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts
new file mode 100644
index 0000000000000..5b7ca99ffc709
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Incident, PartialIncident, ResponseError, ServiceNowError } from './types';
+import { FIELD_PREFIX } from './config';
+import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils';
+import * as i18n from './translations';
+
+export const prepareIncident = (useOldApi: boolean, incident: PartialIncident): PartialIncident =>
+ useOldApi
+ ? incident
+ : Object.entries(incident).reduce(
+ (acc, [key, value]) => ({ ...acc, [`${FIELD_PREFIX}${key}`]: value }),
+ {} as Incident
+ );
+
+const createErrorMessage = (errorResponse?: ServiceNowError): string => {
+ if (errorResponse == null) {
+ return 'unknown: errorResponse was null';
+ }
+
+ const { error } = errorResponse;
+ return error != null
+ ? `${error?.message}: ${error?.detail}`
+ : 'unknown: no error in error response';
+};
+
+export const createServiceError = (error: ResponseError, message: string) =>
+ new Error(
+ getErrorMessage(
+ i18n.SERVICENOW,
+ `${message}. Error: ${error.message} Reason: ${createErrorMessage(error.response?.data)}`
+ )
+ );
+
+export const getPushedDate = (timestamp?: string) => {
+ if (timestamp != null) {
+ return new Date(addTimeZoneToDate(timestamp)).toISOString();
+ }
+
+ return new Date().toISOString();
+};
diff --git a/x-pack/plugins/actions/server/constants/connectors.ts b/x-pack/plugins/actions/server/constants/connectors.ts
new file mode 100644
index 0000000000000..f20d499716cf0
--- /dev/null
+++ b/x-pack/plugins/actions/server/constants/connectors.ts
@@ -0,0 +1,12 @@
+/*
+ * 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: Remove when Elastic for ITSM is published.
+export const ENABLE_NEW_SN_ITSM_CONNECTOR = true;
+
+// TODO: Remove when Elastic for Security Operations is published.
+export const ENABLE_NEW_SN_SIR_CONNECTOR = true;
diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts
index c094109a43d97..9f8e62c77e3a7 100644
--- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts
+++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts
@@ -165,6 +165,47 @@ describe('successful migrations', () => {
});
expect(migratedAction).toEqual(action);
});
+
+ test('set isLegacy config property for .servicenow', () => {
+ const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
+ const action = getMockDataForServiceNow();
+ const migratedAction = migration716(action, context);
+
+ expect(migratedAction).toEqual({
+ ...action,
+ attributes: {
+ ...action.attributes,
+ config: {
+ apiUrl: 'https://example.com',
+ isLegacy: true,
+ },
+ },
+ });
+ });
+
+ test('set isLegacy config property for .servicenow-sir', () => {
+ const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
+ const action = getMockDataForServiceNow({ actionTypeId: '.servicenow-sir' });
+ const migratedAction = migration716(action, context);
+
+ expect(migratedAction).toEqual({
+ ...action,
+ attributes: {
+ ...action.attributes,
+ config: {
+ apiUrl: 'https://example.com',
+ isLegacy: true,
+ },
+ },
+ });
+ });
+
+ test('it does not set isLegacy config for other connectors', () => {
+ const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0'];
+ const action = getMockData();
+ const migratedAction = migration716(action, context);
+ expect(migratedAction).toEqual(action);
+ });
});
describe('8.0.0', () => {
@@ -306,3 +347,19 @@ function getMockData(
type: 'action',
};
}
+
+function getMockDataForServiceNow(
+ overwrites: Record = {}
+): SavedObjectUnsanitizedDoc> {
+ return {
+ attributes: {
+ name: 'abc',
+ actionTypeId: '.servicenow',
+ config: { apiUrl: 'https://example.com' },
+ secrets: { user: 'test', password: '123' },
+ ...overwrites,
+ },
+ id: uuid.v4(),
+ type: 'action',
+ };
+}
diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts
index e75f3eb41f2df..688839eb89858 100644
--- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts
+++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts
@@ -59,13 +59,16 @@ export function getActionsMigrations(
const migrationActionsFourteen = createEsoMigration(
encryptedSavedObjects,
(doc): doc is SavedObjectUnsanitizedDoc => true,
- pipeMigrations(addisMissingSecretsField)
+ pipeMigrations(addIsMissingSecretsField)
);
- const migrationEmailActionsSixteen = createEsoMigration(
+ const migrationActionsSixteen = createEsoMigration(
encryptedSavedObjects,
- (doc): doc is SavedObjectUnsanitizedDoc => doc.attributes.actionTypeId === '.email',
- pipeMigrations(setServiceConfigIfNotSet)
+ (doc): doc is SavedObjectUnsanitizedDoc =>
+ doc.attributes.actionTypeId === '.servicenow' ||
+ doc.attributes.actionTypeId === '.servicenow-sir' ||
+ doc.attributes.actionTypeId === '.email',
+ pipeMigrations(markOldServiceNowITSMConnectorAsLegacy, setServiceConfigIfNotSet)
);
const migrationActions800 = createEsoMigration(
@@ -79,7 +82,7 @@ export function getActionsMigrations(
'7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'),
'7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'),
'7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'),
- '7.16.0': executeMigrationWithErrorHandling(migrationEmailActionsSixteen, '7.16.0'),
+ '7.16.0': executeMigrationWithErrorHandling(migrationActionsSixteen, '7.16.0'),
'8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'),
};
}
@@ -182,7 +185,7 @@ const setServiceConfigIfNotSet = (
};
};
-const addisMissingSecretsField = (
+const addIsMissingSecretsField = (
doc: SavedObjectUnsanitizedDoc
): SavedObjectUnsanitizedDoc => {
return {
@@ -194,6 +197,28 @@ const addisMissingSecretsField = (
};
};
+const markOldServiceNowITSMConnectorAsLegacy = (
+ doc: SavedObjectUnsanitizedDoc
+): SavedObjectUnsanitizedDoc => {
+ if (
+ doc.attributes.actionTypeId !== '.servicenow' &&
+ doc.attributes.actionTypeId !== '.servicenow-sir'
+ ) {
+ return doc;
+ }
+
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ config: {
+ ...doc.attributes.config,
+ isLegacy: true,
+ },
+ },
+ };
+};
+
function pipeMigrations(...migrations: ActionMigration[]): ActionMigration {
return (doc: SavedObjectUnsanitizedDoc) =>
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
diff --git a/x-pack/plugins/apm/dev_docs/apm_queries.md b/x-pack/plugins/apm/dev_docs/apm_queries.md
index 8508e5a173c85..0fbcd4fc1c8a8 100644
--- a/x-pack/plugins/apm/dev_docs/apm_queries.md
+++ b/x-pack/plugins/apm/dev_docs/apm_queries.md
@@ -1,7 +1,17 @@
-# Data model
+### Table of Contents
+ - [Transactions](#transactions)
+ - [System metrics](#system-metrics)
+ - [Transaction breakdown metrics](#transaction-breakdown-metrics)
+ - [Span breakdown metrics](#span-breakdown-metrics)
+ - [Service destination metrics](#service-destination-metrics)
+ - [Common filters](#common-filters)
+
+---
+
+### Data model
Elastic APM agents capture different types of information from within their instrumented applications. These are known as events, and can be spans, transactions, errors, or metrics. You can find more information [here](https://www.elastic.co/guide/en/apm/get-started/current/apm-data-model.html).
-# Running examples
+### Running examples
You can run the example queries on the [edge cluster](https://edge-oblt.elastic.dev/) or any another cluster that contains APM data.
# Transactions
@@ -307,7 +317,7 @@ The above example is overly simplified. In reality [we do a bit more](https://gi
-# Transaction breakdown metrics (`transaction_breakdown`)
+# Transaction breakdown metrics
A pre-aggregations of transaction documents where `transaction.breakdown.count` is the number of original transactions.
@@ -327,7 +337,7 @@ Noteworthy fields: `transaction.name`, `transaction.type`
}
```
-# Span breakdown metrics (`span_breakdown`)
+# Span breakdown metrics
A pre-aggregations of span documents where `span.self_time.count` is the number of original spans. Measures the "self-time" for a span type, and optional subtype, within a transaction group.
@@ -482,7 +492,7 @@ GET apm-*-metric-*,metrics-apm*/_search?terminate_after=1000
}
```
-## Common filters
+# Common filters
Most Elasticsearch queries will need to have one or more filters. There are a couple of reasons for adding filters:
diff --git a/x-pack/plugins/apm/dev_docs/linting.md b/x-pack/plugins/apm/dev_docs/linting.md
index a4fd3094f121c..7db7053e59061 100644
--- a/x-pack/plugins/apm/dev_docs/linting.md
+++ b/x-pack/plugins/apm/dev_docs/linting.md
@@ -1,6 +1,6 @@
-## Linting
+# Linting
-_Note: Run the following commands from the root of Kibana._
+_Note: Run the commands from the root of Kibana._
### Typescript
@@ -19,4 +19,3 @@ yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write
```
node scripts/eslint.js x-pack/legacy/plugins/apm
```
-diff --git a/x-pack/plugins/apm/dev_docs/feature_flags.md b/x-pack/plugins/apm/dev_docs/feature_flags.md
diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md
index 4d0edc27fe644..ba48e7e229e27 100644
--- a/x-pack/plugins/apm/dev_docs/testing.md
+++ b/x-pack/plugins/apm/dev_docs/testing.md
@@ -64,3 +64,13 @@ node scripts/functional_test_runner --config x-pack/test/functional/config.js --
APM tests are located in `x-pack/test/functional/apps/apm`.
For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme)
diff --git a/x-pack/plugins/apm/scripts/test/README.md b/x-pack/plugins/apm/scripts/test/README.md
+
+
+## Storybook
+
+### Start
+```
+yarn storybook apm
+```
+
+All files with a .stories.tsx extension will be loaded. You can access the development environment at http://localhost:9001.
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts
index 1e954d9982295..47eba11e6f6fb 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-/* eslint-disable @typescript-eslint/naming-convention */
const apmIndicesSaveURL = '/internal/apm/settings/apm-indices/save';
@@ -20,12 +19,12 @@ describe('No data screen', () => {
url: apmIndicesSaveURL,
method: 'POST',
body: {
- 'apm_oss.sourcemapIndices': 'foo-*',
- 'apm_oss.errorIndices': 'foo-*',
- 'apm_oss.onboardingIndices': 'foo-*',
- 'apm_oss.spanIndices': 'foo-*',
- 'apm_oss.transactionIndices': 'foo-*',
- 'apm_oss.metricsIndices': 'foo-*',
+ sourcemaps: 'foo-*',
+ errors: 'foo-*',
+ onboarding: 'foo-*',
+ spans: 'foo-*',
+ transactions: 'foo-*',
+ metrics: 'foo-*',
},
headers: {
'kbn-xsrf': true,
@@ -50,12 +49,12 @@ describe('No data screen', () => {
url: apmIndicesSaveURL,
method: 'POST',
body: {
- 'apm_oss.sourcemapIndices': '',
- 'apm_oss.errorIndices': '',
- 'apm_oss.onboardingIndices': '',
- 'apm_oss.spanIndices': '',
- 'apm_oss.transactionIndices': '',
- 'apm_oss.metricsIndices': '',
+ sourcemaps: '',
+ errors: '',
+ onboarding: '',
+ spans: '',
+ transactions: '',
+ metrics: '',
},
headers: { 'kbn-xsrf': true },
auth: { user: 'apm_power_user', pass: 'changeme' },
diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json
index 4e82d82d655b4..865358959ea72 100644
--- a/x-pack/plugins/apm/kibana.json
+++ b/x-pack/plugins/apm/kibana.json
@@ -8,7 +8,6 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"requiredPlugins": [
- "apmOss",
"data",
"embeddable",
"features",
diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx
index 8a54c76df0f69..ee6a58b0dbb76 100644
--- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx
+++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx
@@ -96,7 +96,7 @@ export function ChartPreview({
position={Position.Left}
tickFormat={yTickFormat}
ticks={5}
- domain={{ max: yMax }}
+ domain={{ max: yMax, min: NaN }}
/>
{
+ const onBrushEnd = ({ x }: XYBrushEvent) => {
if (!x) {
return;
}
@@ -99,7 +100,7 @@ export function PageLoadDistChart({
diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx
index 6685dddd87d7f..2e526eff04346 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx
@@ -30,40 +30,40 @@ import {
const APM_INDEX_LABELS = [
{
- configurationName: 'apm_oss.sourcemapIndices',
+ configurationName: 'sourcemap',
label: i18n.translate(
'xpack.apm.settings.apmIndices.sourcemapIndicesLabel',
{ defaultMessage: 'Sourcemap Indices' }
),
},
{
- configurationName: 'apm_oss.errorIndices',
+ configurationName: 'error',
label: i18n.translate('xpack.apm.settings.apmIndices.errorIndicesLabel', {
defaultMessage: 'Error Indices',
}),
},
{
- configurationName: 'apm_oss.onboardingIndices',
+ configurationName: 'onboarding',
label: i18n.translate(
'xpack.apm.settings.apmIndices.onboardingIndicesLabel',
{ defaultMessage: 'Onboarding Indices' }
),
},
{
- configurationName: 'apm_oss.spanIndices',
+ configurationName: 'span',
label: i18n.translate('xpack.apm.settings.apmIndices.spanIndicesLabel', {
defaultMessage: 'Span Indices',
}),
},
{
- configurationName: 'apm_oss.transactionIndices',
+ configurationName: 'transaction',
label: i18n.translate(
'xpack.apm.settings.apmIndices.transactionIndicesLabel',
{ defaultMessage: 'Transaction Indices' }
),
},
{
- configurationName: 'apm_oss.metricsIndices',
+ configurationName: 'metric',
label: i18n.translate('xpack.apm.settings.apmIndices.metricsIndicesLabel', {
defaultMessage: 'Metrics Indices',
}),
@@ -145,7 +145,7 @@ export function ApmIndices() {
}
),
});
- } catch (error) {
+ } catch (error: any) {
notifications.toasts.addDanger({
title: i18n.translate(
'xpack.apm.settings.apmIndices.applyChanges.failed.title',
@@ -215,7 +215,10 @@ export function ApmIndices() {
{
defaultMessage:
'Overrides {configurationName}: {defaultValue}',
- values: { configurationName, defaultValue },
+ values: {
+ configurationName: `xpack.apm.indices.${configurationName}`,
+ defaultValue,
+ },
}
)}
fullWidth
diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx
index a2a92b7e16f8e..12fa1c955ccc8 100644
--- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx
@@ -15,12 +15,6 @@ import props from './__fixtures__/props.json';
import { MemoryRouter } from 'react-router-dom';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
-jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
- return {
- htmlIdGenerator: () => () => `generated-id`,
- };
-});
-
describe('ErrorGroupOverview -> List', () => {
beforeAll(() => {
mockMoment();
diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap
index 890c692096a66..c8c7bf82dff04 100644
--- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap
@@ -56,7 +56,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
List should render with data 1`] = `
List should render with data 1`] = `
className="euiPagination__item"
>
void;
onClearSelection: () => void;
selection?: Selection;
traceSamples: TabContentProps['traceSamples'];
@@ -126,10 +126,8 @@ export function TransactionDistribution({
const trackApmEvent = useUiTracker({ app: 'apm' });
- const onTrackedChartSelection: BrushEndListener = (
- brushArea: XYBrushArea
- ) => {
- onChartSelection(brushArea);
+ const onTrackedChartSelection = (brushEvent: XYBrushEvent) => {
+ onChartSelection(brushEvent);
trackApmEvent({ metric: 'transaction_distribution_chart_selection' });
};
@@ -216,7 +214,7 @@ export function TransactionDistribution({
markerCurrentTransaction={markerCurrentTransaction}
markerPercentile={DEFAULT_PERCENTILE_THRESHOLD}
markerValue={response.percentileThresholdValue ?? 0}
- onChartSelection={onTrackedChartSelection}
+ onChartSelection={onTrackedChartSelection as BrushEndListener}
hasData={hasData}
selection={selection}
status={status}
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx
index b249161980586..9ccca9886e679 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx
@@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { omit } from 'lodash';
import { useHistory } from 'react-router-dom';
-import { XYBrushArea } from '@elastic/charts';
+import { XYBrushEvent } from '@elastic/charts';
import { EuiPanel, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
@@ -48,7 +48,7 @@ export function TransactionDetailsTabs() {
environment,
});
- const selectSampleFromChartSelection = (selection: XYBrushArea) => {
+ const selectSampleFromChartSelection = (selection: XYBrushEvent) => {
if (selection !== undefined) {
const { x } = selection;
if (Array.isArray(x)) {
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/types.ts b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts
index 1ccb3d01a9b28..c3d2b9648e82a 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/types.ts
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { XYBrushArea } from '@elastic/charts';
+import { XYBrushEvent } from '@elastic/charts';
import type { TraceSample } from '../../../hooks/use_transaction_trace_samples_fetcher';
@@ -14,6 +14,6 @@ export interface TabContentProps {
onFilter: () => void;
sampleRangeFrom?: number;
sampleRangeTo?: number;
- selectSampleFromChartSelection: (selection: XYBrushArea) => void;
+ selectSampleFromChartSelection: (selection: XYBrushEvent) => void;
traceSamples: TraceSample[];
}
diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx
index de9c7f651019e..8f66658785b97 100644
--- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx
+++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx
@@ -24,7 +24,6 @@ import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import { AgentIcon } from '../../shared/agent_icon';
import { NewPackagePolicy } from '../apm_policy_form/typings';
import { getCommands } from '../../../tutorial/config_agent/commands/get_commands';
-import { CopyCommands } from '../../../tutorial/config_agent/copy_commands';
import { replaceTemplateStrings } from './replace_template_strings';
function AccordionButtonContent({
@@ -91,14 +90,9 @@ function TutorialConfigAgent({
policyDetails: { apmServerUrl, secretToken },
});
return (
-
-
-
-
-
- {commandBlock}
-
-
+
+ {commandBlock}
+
);
}
@@ -153,23 +147,16 @@ export function AgentInstructionsAccordion({
{textPre && (
-
-
-
-
- {commandBlock && (
-
-
-
- )}
-
+
)}
{commandBlock && (
<>
- {commandBlock}
+
+ {commandBlock}
+
>
)}
{customComponentName === 'TutorialConfigAgent' && (
diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.test.tsx
index c73d312e0cf18..6bab77dbe4970 100644
--- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.test.tsx
@@ -12,12 +12,6 @@ import { mountWithTheme } from '../../../utils/testHelpers';
import { Stackframe as StackframeComponent } from './Stackframe';
import stacktracesMock from './__fixtures__/stacktraces.json';
-jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
- return {
- htmlIdGenerator: () => () => `generated-id`,
- };
-});
-
describe('Stackframe', () => {
describe('when stackframe has source lines', () => {
let wrapper: ReactWrapper;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx
index 9dc2fbd4cc961..16157071affcd 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx
@@ -17,6 +17,7 @@ import {
ScaleType,
Settings,
TickFormatter,
+ XYBrushEvent,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -27,8 +28,8 @@ import { Annotation } from '../../../../../common/annotations';
import { useChartTheme } from '../../../../../../observability/public';
import {
asAbsoluteDateTime,
- asDuration,
asPercent,
+ getDurationFormatter,
} from '../../../../../common/utils/formatters';
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
import { useChartPointerEventContext } from '../../../../context/chart_pointer_event/use_chart_pointer_event_context';
@@ -39,6 +40,10 @@ import { ChartContainer } from '../../charts/chart_container';
import { isTimeseriesEmpty, onBrushEnd } from '../../charts/helper/helper';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useTimeRange } from '../../../../hooks/use_time_range';
+import {
+ getMaxY,
+ getResponseTimeTickFormatter,
+} from '../../../shared/charts/transaction_charts/helper';
interface Props {
fetchStatus: FETCH_STATUS;
@@ -50,7 +55,6 @@ interface Props {
}
const asPercentBound = (y: number | null) => asPercent(y, 1);
-const asDurationBound = (y: number | null) => asDuration(y);
export function BreakdownChart({
fetchStatus,
@@ -82,15 +86,20 @@ export function BreakdownChart({
const isEmpty = isTimeseriesEmpty(timeseries);
+ const maxY = getMaxY(timeseries);
const yTickFormat: TickFormatter =
- yAxisType === 'duration' ? asDurationBound : asPercentBound;
+ yAxisType === 'duration'
+ ? getResponseTimeTickFormatter(getDurationFormatter(maxY))
+ : asPercentBound;
return (
onBrushEnd({ x, history })}
+ onBrushEnd={(event) =>
+ onBrushEnd({ x: (event as XYBrushEvent).x, history })
+ }
showLegend
showLegendExtra
legendPosition={Position.Bottom}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts
index d94f2ce8f5c5d..9dccddd509387 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts
+++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { XYBrushArea } from '@elastic/charts';
+import { XYBrushEvent } from '@elastic/charts';
import { History } from 'history';
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
import { fromQuery, toQuery } from '../../Links/url_helpers';
@@ -14,7 +14,7 @@ export const onBrushEnd = ({
x,
history,
}: {
- x: XYBrushArea['x'];
+ x: XYBrushEvent['x'];
history: History;
}) => {
if (x) {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx
index 9ee77cd95ee0d..9f437a95e7dd9 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx
@@ -9,9 +9,9 @@ import { EuiTitle } from '@elastic/eui';
import React from 'react';
import {
asDecimal,
- asDuration,
asInteger,
asPercent,
+ getDurationFormatter,
getFixedByteFormatter,
} from '../../../../../common/utils/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@@ -19,22 +19,24 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform
import { Maybe } from '../../../../../typings/common';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { TimeseriesChart } from '../timeseries_chart';
+import {
+ getMaxY,
+ getResponseTimeTickFormatter,
+} from '../transaction_charts/helper';
function getYTickFormatter(chart: GenericMetricsChart) {
+ const max = getMaxY(chart.series);
+
switch (chart.yUnit) {
case 'bytes': {
- const max = Math.max(
- ...chart.series.map(({ data }) =>
- Math.max(...data.map(({ y }) => y || 0))
- )
- );
return getFixedByteFormatter(max);
}
case 'percent': {
return (y: Maybe) => asPercent(y || 0, 1);
}
case 'time': {
- return asDuration;
+ const durationFormatter = getDurationFormatter(max);
+ return getResponseTimeTickFormatter(durationFormatter);
}
case 'integer': {
return asInteger;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx
index 65ecdec0f36a5..08e8908d50e7a 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx
@@ -20,6 +20,7 @@ import {
ScaleType,
Settings,
YDomainRange,
+ XYBrushEvent,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -115,7 +116,9 @@ export function TimeseriesChart({
onBrushEnd({ x, history })}
+ onBrushEnd={(event) =>
+ onBrushEnd({ x: (event as XYBrushEvent).x, history })
+ }
theme={{
...chartTheme,
areaSeriesStyle: {
diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap
index e4674b3add880..15613af4daf98 100644
--- a/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap
@@ -35,7 +35,12 @@ exports[`ManagedTable should render a page-full of items, with defaults 1`] = `
]
}
loading={false}
- noItemsMessage="No items found"
+ noItemsMessage={
+
+ }
onChange={[Function]}
pagination={
Object {
@@ -85,7 +90,12 @@ exports[`ManagedTable should render when specifying initial values 1`] = `
]
}
loading={false}
- noItemsMessage="No items found"
+ noItemsMessage={
+
+ }
onChange={[Function]}
pagination={
Object {
diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx
index 617af6dae484d..abdab939f4a0a 100644
--- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx
+++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx
@@ -100,7 +100,7 @@ const urlService = new UrlService({
getUrl: async ({ app, path }, { absolute }) => {
return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`;
},
- shortUrls: {} as any,
+ shortUrls: () => ({ get: () => {} } as any),
});
const locator = urlService.locators.create(new MlLocatorDefinition());
diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx
deleted file mode 100644
index c5261cfc1dc04..0000000000000
--- a/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx
+++ /dev/null
@@ -1,26 +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 { EuiButton, EuiCopy } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-
-interface Props {
- commands: string;
-}
-export function CopyCommands({ commands }: Props) {
- return (
-
- {(copy) => (
-
- {i18n.translate('xpack.apm.tutorial.copySnippet', {
- defaultMessage: 'Copy snippet',
- })}
-
- )}
-
- );
-}
diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx
index 5ff1fd7f42119..bce16ae6ef1f9 100644
--- a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx
+++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx
@@ -4,20 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import {
- EuiCodeBlock,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiCodeBlock, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { HttpStart } from 'kibana/public';
import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { APIReturnType } from '../..//services/rest/createCallApmApi';
import { getCommands } from './commands/get_commands';
-import { CopyCommands } from './copy_commands';
import { getPolicyOptions, PolicyOption } from './get_policy_options';
import { PolicySelector } from './policy_selector';
@@ -136,27 +129,19 @@ function TutorialConfigAgent({
return (
<>
-
-
-
- setSelectedOption(newSelectedOption)
- }
- fleetLink={getFleetLink({
- isFleetEnabled: data.isFleetEnabled,
- hasFleetAgents,
- basePath,
- })}
- />
-
-
-
-
-
+ setSelectedOption(newSelectedOption)}
+ fleetLink={getFleetLink({
+ isFleetEnabled: data.isFleetEnabled,
+ hasFleetAgents,
+ basePath,
+ })}
+ />
+
-
+
{commands}
>
diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx
index 8764ac48c5440..2203bc63f68cd 100644
--- a/x-pack/plugins/apm/public/utils/testHelpers.tsx
+++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx
@@ -119,14 +119,12 @@ interface MockSetup {
config: APMConfig;
uiFilters: UxUIFilters;
indices: {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': string;
- 'apm_oss.errorIndices': string;
- 'apm_oss.onboardingIndices': string;
- 'apm_oss.spanIndices': string;
- 'apm_oss.transactionIndices': string;
- 'apm_oss.metricsIndices': string;
- /* eslint-enable @typescript-eslint/naming-convention */
+ sourcemaps: string;
+ errors: string;
+ onboarding: string;
+ spans: string;
+ transactions: string;
+ metrics: string;
apmAgentConfigurationIndex: string;
apmCustomLinkIndex: string;
};
@@ -178,14 +176,12 @@ export async function inspectSearchParams(
) as APMConfig,
uiFilters: {},
indices: {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': 'myIndex',
- 'apm_oss.errorIndices': 'myIndex',
- 'apm_oss.onboardingIndices': 'myIndex',
- 'apm_oss.spanIndices': 'myIndex',
- 'apm_oss.transactionIndices': 'myIndex',
- 'apm_oss.metricsIndices': 'myIndex',
- /* eslint-enable @typescript-eslint/naming-convention */
+ sourcemaps: 'myIndex',
+ errors: 'myIndex',
+ onboarding: 'myIndex',
+ spans: 'myIndex',
+ transactions: 'myIndex',
+ metrics: 'myIndex',
apmAgentConfigurationIndex: 'myIndex',
apmCustomLinkIndex: 'myIndex',
},
diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md
index 6ebe07d44683c..040e620ae91ca 100644
--- a/x-pack/plugins/apm/readme.md
+++ b/x-pack/plugins/apm/readme.md
@@ -1,31 +1,18 @@
# Documentation for APM UI developers
-## Local environment setup
+## Getting started
+- [Local setup](./dev_docs/local_setup.md)
+- [Testing (unit, api, e2e, storybook)](./dev_docs/testing.md)
+- [Linting (typescript, eslint, prettier)](./dev_docs/linting.md)
-[Local setup documentation](./dev_docs/local_setup.md)
-
-## Testing
-
-[Testing documentation](./dev_docs/testing.md)
-
-## Linting
-
-[Linting documentation](./dev_docs/linting.md)
-
-## Storybook
-
-**Start**
-```
-yarn storybook apm
-```
-
-All files with a .stories.tsx extension will be loaded. You can access the development environment at http://localhost:9001.
-
-## Further resources
+## APM concepts
- [Queries and data model](./dev_docs/apm_queries.md)
+- [Telemetry](./dev_docs/telemetry.md)
+- [Routing and Linking](./dev_docs/routing_and_linking.md)
+
+## Tooling
- [VSCode setup instructions](./dev_docs/vscode_setup.md)
- [Github PR commands](./dev_docs/github_commands.md)
-- [Routing and Linking](./dev_docs/routing_and_linking.md)
-- [Telemetry](./dev_docs/telemetry.md)
-- [Features flags](./dev_docs/feature_flags.md)
+
+## Other resources
- [Official APM UI settings docs](https://www.elastic.co/guide/en/kibana/current/apm-settings-in-kibana.html)
diff --git a/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts b/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts
index a85bd007bc4f3..f3e2b48390468 100644
--- a/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts
+++ b/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts
@@ -38,14 +38,12 @@ export const readKibanaConfig = () => {
};
return {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.transactionIndices': 'apm-*',
- 'apm_oss.metricsIndices': 'apm-*',
- 'apm_oss.errorIndices': 'apm-*',
- 'apm_oss.spanIndices': 'apm-*',
- 'apm_oss.onboardingIndices': 'apm-*',
- 'apm_oss.sourcemapIndices': 'apm-*',
- /* eslint-enable @typescript-eslint/naming-convention */
+ 'xpack.apm.indices.transaction': 'traces-apm*,apm-*',
+ 'xpack.apm.indices.metric': 'metrics-apm*,apm-*',
+ 'xpack.apm.indices.error': 'logs-apm*,apm-*',
+ 'xpack.apm.indices.span': 'traces-apm*,apm-*',
+ 'xpack.apm.indices.onboarding': 'apm-*',
+ 'xpack.apm.indices.sourcemap': 'apm-*',
'elasticsearch.hosts': 'http://localhost:9200',
...loadedKibanaConfig,
...cliEsCredentials,
diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts
index 0dab75cfba9c7..c900123c6cee9 100644
--- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts
+++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts
@@ -78,7 +78,12 @@ async function uploadData() {
collectTelemetryParams: {
logger: console as unknown as Logger,
indices: {
- ...config,
+ transaction: config['xpack.apm.indices.transaction'],
+ metric: config['xpack.apm.indices.metric'],
+ error: config['xpack.apm.indices.error'],
+ span: config['xpack.apm.indices.span'],
+ onboarding: config['xpack.apm.indices.onboarding'],
+ sourcemap: config['xpack.apm.indices.sourcemap'],
apmCustomLinkIndex: '.apm-custom-links',
apmAgentConfigurationIndex: '.apm-agent-configuration',
},
diff --git a/x-pack/plugins/apm/server/index.test.ts b/x-pack/plugins/apm/server/index.test.ts
deleted file mode 100644
index be93557fea6fc..0000000000000
--- a/x-pack/plugins/apm/server/index.test.ts
+++ /dev/null
@@ -1,40 +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.
- */
-
-/* eslint-disable @typescript-eslint/naming-convention */
-
-import { APMOSSConfig } from 'src/plugins/apm_oss/server';
-import { APMXPackConfig } from '.';
-import { mergeConfigs } from './index';
-
-describe('mergeConfigs', () => {
- it('merges the configs', () => {
- const apmOssConfig = {
- transactionIndices: 'apm-*-transaction-*',
- spanIndices: 'apm-*-span-*',
- errorIndices: 'apm-*-error-*',
- metricsIndices: 'apm-*-metric-*',
- } as APMOSSConfig;
-
- const apmConfig = {
- ui: { enabled: false },
- enabled: true,
- metricsInterval: 2000,
- agent: { migrations: { enabled: true } },
- } as APMXPackConfig;
-
- expect(mergeConfigs(apmOssConfig, apmConfig)).toEqual({
- 'apm_oss.errorIndices': 'logs-apm*,apm-*-error-*',
- 'apm_oss.metricsIndices': 'metrics-apm*,apm-*-metric-*',
- 'apm_oss.spanIndices': 'traces-apm*,apm-*-span-*',
- 'apm_oss.transactionIndices': 'traces-apm*,apm-*-transaction-*',
- 'xpack.apm.metricsInterval': 2000,
- 'xpack.apm.ui.enabled': false,
- 'xpack.apm.agent.migrations.enabled': true,
- });
- });
-});
diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts
index 22787b0301ce0..c0dffc50e4e4f 100644
--- a/x-pack/plugins/apm/server/index.ts
+++ b/x-pack/plugins/apm/server/index.ts
@@ -10,11 +10,12 @@ import {
PluginConfigDescriptor,
PluginInitializerContext,
} from 'src/core/server';
-import { APMOSSConfig } from 'src/plugins/apm_oss/server';
import { maxSuggestions } from '../../observability/common';
import { SearchAggregatedTransactionSetting } from '../common/aggregated_transactions';
import { APMPlugin } from './plugin';
+// All options should be documented in the APM configuration settings: https://github.com/elastic/kibana/blob/master/docs/settings/apm-settings.asciidoc
+// and be included on cloud allow list unless there are specific reasons not to
const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
serviceMapEnabled: schema.boolean({ defaultValue: true }),
@@ -47,12 +48,37 @@ const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
}),
}),
+ indices: schema.object({
+ transaction: schema.string({ defaultValue: 'traces-apm*,apm-*' }),
+ span: schema.string({ defaultValue: 'traces-apm*,apm-*' }),
+ error: schema.string({ defaultValue: 'logs-apm*,apm-*' }),
+ metric: schema.string({ defaultValue: 'metrics-apm*,apm-*' }),
+ sourcemap: schema.string({ defaultValue: 'apm-*' }),
+ onboarding: schema.string({ defaultValue: 'apm-*' }),
+ }),
});
// plugin config
-export const config: PluginConfigDescriptor = {
- deprecations: ({ deprecate, renameFromRoot }) => [
+export const config: PluginConfigDescriptor = {
+ deprecations: ({
+ deprecate,
+ renameFromRoot,
+ deprecateFromRoot,
+ unusedFromRoot,
+ }) => [
deprecate('enabled', '8.0.0'),
+ renameFromRoot(
+ 'apm_oss.transactionIndices',
+ 'xpack.apm.indices.transaction'
+ ),
+ renameFromRoot('apm_oss.spanIndices', 'xpack.apm.indices.span'),
+ renameFromRoot('apm_oss.errorIndices', 'xpack.apm.indices.error'),
+ renameFromRoot('apm_oss.metricsIndices', 'xpack.apm.indices.metric'),
+ renameFromRoot('apm_oss.sourcemapIndices', 'xpack.apm.indices.sourcemap'),
+ renameFromRoot('apm_oss.onboardingIndices', 'xpack.apm.indices.onboarding'),
+ deprecateFromRoot('apm_oss.enabled', '8.0.0'),
+ unusedFromRoot('apm_oss.fleetMode'),
+ unusedFromRoot('apm_oss.indexPattern'),
renameFromRoot(
'xpack.apm.maxServiceEnvironments',
`uiSettings.overrides[${maxSuggestions}]`
@@ -70,69 +96,8 @@ export const config: PluginConfigDescriptor = {
schema: configSchema,
};
-export type APMXPackConfig = TypeOf;
-export type APMConfig = ReturnType;
-
-// plugin config and ui indices settings
-// All options should be documented in the APM configuration settings: https://github.com/elastic/kibana/blob/master/docs/settings/apm-settings.asciidoc
-// and be included on cloud allow list unless there are specific reasons not to
-export function mergeConfigs(
- apmOssConfig: APMOSSConfig,
- apmConfig: APMXPackConfig
-) {
- const mergedConfig = {
- /* eslint-disable @typescript-eslint/naming-convention */
- // TODO: Remove all apm_oss options by 8.0
- 'apm_oss.transactionIndices': apmOssConfig.transactionIndices,
- 'apm_oss.spanIndices': apmOssConfig.spanIndices,
- 'apm_oss.errorIndices': apmOssConfig.errorIndices,
- 'apm_oss.metricsIndices': apmOssConfig.metricsIndices,
- 'apm_oss.sourcemapIndices': apmOssConfig.sourcemapIndices,
- 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices,
- /* eslint-enable @typescript-eslint/naming-convention */
- 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled,
- 'xpack.apm.serviceMapFingerprintBucketSize':
- apmConfig.serviceMapFingerprintBucketSize,
- 'xpack.apm.serviceMapTraceIdBucketSize':
- apmConfig.serviceMapTraceIdBucketSize,
- 'xpack.apm.serviceMapFingerprintGlobalBucketSize':
- apmConfig.serviceMapFingerprintGlobalBucketSize,
- 'xpack.apm.serviceMapTraceIdGlobalBucketSize':
- apmConfig.serviceMapTraceIdGlobalBucketSize,
- 'xpack.apm.serviceMapMaxTracesPerRequest':
- apmConfig.serviceMapMaxTracesPerRequest,
- 'xpack.apm.ui.enabled': apmConfig.ui.enabled,
- 'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems,
- 'xpack.apm.ui.transactionGroupBucketSize':
- apmConfig.ui.transactionGroupBucketSize,
- 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern,
- 'xpack.apm.telemetryCollectionEnabled':
- apmConfig.telemetryCollectionEnabled,
- 'xpack.apm.searchAggregatedTransactions':
- apmConfig.searchAggregatedTransactions,
- 'xpack.apm.metricsInterval': apmConfig.metricsInterval,
- 'xpack.apm.agent.migrations.enabled': apmConfig.agent.migrations.enabled,
- };
-
- // Add data stream indices to list of configured values
- mergedConfig[
- 'apm_oss.transactionIndices'
- ] = `traces-apm*,${mergedConfig['apm_oss.transactionIndices']}`;
-
- mergedConfig[
- 'apm_oss.spanIndices'
- ] = `traces-apm*,${mergedConfig['apm_oss.spanIndices']}`;
-
- mergedConfig[
- 'apm_oss.errorIndices'
- ] = `logs-apm*,${mergedConfig['apm_oss.errorIndices']}`;
-
- mergedConfig[
- 'apm_oss.metricsIndices'
- ] = `metrics-apm*,${mergedConfig['apm_oss.metricsIndices']}`;
-
- return mergedConfig;
-}
+export type APMConfig = TypeOf;
+export type ApmIndicesConfigName = keyof APMConfig['indices'];
export const plugin = (initContext: PluginInitializerContext) =>
new APMPlugin(initContext);
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
index d1026b0b6ca8b..7fe2adcfe24d7 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
@@ -99,7 +99,7 @@ export function registerErrorCountAlertType({
});
const searchParams = {
- index: indices['apm_oss.errorIndices'],
+ index: indices.error,
size: 0,
body: {
query: {
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
index b383b4777eca4..df4de254346c9 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
@@ -115,12 +115,12 @@ export function registerTransactionDurationAlertType({
// to prevent (likely) unnecessary blocking request
// in rule execution
const searchAggregatedTransactions =
- config['xpack.apm.searchAggregatedTransactions'] !==
+ config.searchAggregatedTransactions !==
SearchAggregatedTransactionSetting.never;
const index = searchAggregatedTransactions
- ? indices['apm_oss.metricsIndices']
- : indices['apm_oss.transactionIndices'];
+ ? indices.metric
+ : indices.transaction;
const field = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
index 6c59bcc4107b0..598487d02625a 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
@@ -110,12 +110,12 @@ export function registerTransactionErrorRateAlertType({
// to prevent (likely) unnecessary blocking request
// in rule execution
const searchAggregatedTransactions =
- config['xpack.apm.searchAggregatedTransactions'] !==
+ config.searchAggregatedTransactions !==
SearchAggregatedTransactionSetting.never;
const index = searchAggregatedTransactions
- ? indices['apm_oss.metricsIndices']
- : indices['apm_oss.transactionIndices'];
+ ? indices.metric
+ : indices.transaction;
const searchParams = {
index,
diff --git a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts
index 5d5865bdd2289..22649a7010461 100644
--- a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts
@@ -17,10 +17,10 @@ export const createRuleTypeMocks = () => {
let alertExecutor: (...args: any[]) => Promise;
const mockedConfig$ = of({
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.errorIndices': 'apm-*',
- 'apm_oss.transactionIndices': 'apm-*',
- /* eslint-enable @typescript-eslint/naming-convention */
+ indices: {
+ error: 'apm-*',
+ transaction: 'apm-*',
+ },
} as APMConfig);
const loggerMock = {
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
index 324202b207237..10758b6d90cdc 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
@@ -50,7 +50,7 @@ export async function createAnomalyDetectionJobs(
`Creating ML anomaly detection jobs for environments: [${uniqueMlJobEnvs}].`
);
- const indexPatternName = indices['apm_oss.metricsIndices'];
+ const indexPatternName = indices.metric;
const responses = await Promise.all(
uniqueMlJobEnvs.map((environment) =>
createAnomalyDetectionJob({ ml, environment, indexPatternName })
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
index 4bfac442b4a3c..1e697ebdcae06 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
@@ -14,12 +14,10 @@ import {
describe('data telemetry collection tasks', () => {
const indices = {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.errorIndices': 'apm-8.0.0-error',
- 'apm_oss.metricsIndices': 'apm-8.0.0-metric',
- 'apm_oss.spanIndices': 'apm-8.0.0-span',
- 'apm_oss.transactionIndices': 'apm-8.0.0-transaction',
- /* eslint-enable @typescript-eslint/naming-convention */
+ error: 'apm-8.0.0-error',
+ metric: 'apm-8.0.0-metric',
+ span: 'apm-8.0.0-span',
+ transaction: 'apm-8.0.0-transaction',
} as ApmIndicesConfig;
describe('environments', () => {
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
index d624c8527df86..8764223ad1ebb 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
@@ -78,7 +78,7 @@ export const tasks: TelemetryTask[] = [
};
const params = {
- index: [indices['apm_oss.transactionIndices']],
+ index: [indices.transaction],
body: {
size: 0,
timeout,
@@ -138,7 +138,7 @@ export const tasks: TelemetryTask[] = [
// fixed date range for reliable results
const lastTransaction = (
await search({
- index: indices['apm_oss.transactionIndices'],
+ index: indices.transaction,
body: {
query: {
bool: {
@@ -253,10 +253,10 @@ export const tasks: TelemetryTask[] = [
const response = await search({
index: [
- indices['apm_oss.errorIndices'],
- indices['apm_oss.metricsIndices'],
- indices['apm_oss.spanIndices'],
- indices['apm_oss.transactionIndices'],
+ indices.error,
+ indices.metric,
+ indices.span,
+ indices.transaction,
],
body: {
size: 0,
@@ -310,10 +310,10 @@ export const tasks: TelemetryTask[] = [
const response = await search({
index: [
- indices['apm_oss.errorIndices'],
- indices['apm_oss.metricsIndices'],
- indices['apm_oss.spanIndices'],
- indices['apm_oss.transactionIndices'],
+ indices.error,
+ indices.metric,
+ indices.span,
+ indices.transaction,
],
body: {
size: 0,
@@ -345,7 +345,7 @@ export const tasks: TelemetryTask[] = [
name: 'environments',
executor: async ({ indices, search }) => {
const response = await search({
- index: [indices['apm_oss.transactionIndices']],
+ index: [indices.transaction],
body: {
query: {
bool: {
@@ -426,12 +426,12 @@ export const tasks: TelemetryTask[] = [
name: 'processor_events',
executor: async ({ indices, search }) => {
const indicesByProcessorEvent = {
- error: indices['apm_oss.errorIndices'],
- metric: indices['apm_oss.metricsIndices'],
- span: indices['apm_oss.spanIndices'],
- transaction: indices['apm_oss.transactionIndices'],
- onboarding: indices['apm_oss.onboardingIndices'],
- sourcemap: indices['apm_oss.sourcemapIndices'],
+ error: indices.error,
+ metric: indices.metric,
+ span: indices.span,
+ transaction: indices.transaction,
+ onboarding: indices.onboarding,
+ sourcemap: indices.sourcemap,
};
type ProcessorEvent = keyof typeof indicesByProcessorEvent;
@@ -549,10 +549,10 @@ export const tasks: TelemetryTask[] = [
return prevJob.then(async (data) => {
const response = await search({
index: [
- indices['apm_oss.errorIndices'],
- indices['apm_oss.spanIndices'],
- indices['apm_oss.metricsIndices'],
- indices['apm_oss.transactionIndices'],
+ indices.error,
+ indices.span,
+ indices.metric,
+ indices.transaction,
],
body: {
size: 0,
@@ -598,11 +598,7 @@ export const tasks: TelemetryTask[] = [
name: 'versions',
executor: async ({ search, indices }) => {
const response = await search({
- index: [
- indices['apm_oss.transactionIndices'],
- indices['apm_oss.spanIndices'],
- indices['apm_oss.errorIndices'],
- ],
+ index: [indices.transaction, indices.span, indices.error],
terminateAfter: 1,
body: {
query: {
@@ -647,7 +643,7 @@ export const tasks: TelemetryTask[] = [
executor: async ({ search, indices }) => {
const errorGroupsCount = (
await search({
- index: indices['apm_oss.errorIndices'],
+ index: indices.error,
body: {
size: 0,
timeout,
@@ -683,7 +679,7 @@ export const tasks: TelemetryTask[] = [
const transactionGroupsCount = (
await search({
- index: indices['apm_oss.transactionIndices'],
+ index: indices.transaction,
body: {
size: 0,
timeout,
@@ -719,7 +715,7 @@ export const tasks: TelemetryTask[] = [
const tracesPerDayCount = (
await search({
- index: indices['apm_oss.transactionIndices'],
+ index: indices.transaction,
body: {
query: {
bool: {
@@ -741,11 +737,7 @@ export const tasks: TelemetryTask[] = [
const servicesCount = (
await search({
- index: [
- indices['apm_oss.transactionIndices'],
- indices['apm_oss.errorIndices'],
- indices['apm_oss.metricsIndices'],
- ],
+ index: [indices.transaction, indices.error, indices.metric],
body: {
size: 0,
timeout,
@@ -811,11 +803,7 @@ export const tasks: TelemetryTask[] = [
const data = await prevJob;
const response = await search({
- index: [
- indices['apm_oss.errorIndices'],
- indices['apm_oss.metricsIndices'],
- indices['apm_oss.transactionIndices'],
- ],
+ index: [indices.error, indices.metric, indices.transaction],
body: {
size: 0,
timeout,
@@ -1006,12 +994,12 @@ export const tasks: TelemetryTask[] = [
const response = await indicesStats({
index: [
indices.apmAgentConfigurationIndex,
- indices['apm_oss.errorIndices'],
- indices['apm_oss.metricsIndices'],
- indices['apm_oss.onboardingIndices'],
- indices['apm_oss.sourcemapIndices'],
- indices['apm_oss.spanIndices'],
- indices['apm_oss.transactionIndices'],
+ indices.error,
+ indices.metric,
+ indices.onboarding,
+ indices.sourcemap,
+ indices.span,
+ indices.transaction,
],
});
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts
index 809869e13de7f..871df10d9bafa 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts
@@ -43,14 +43,12 @@ describe('get buckets', () => {
}
) as APMConfig,
indices: {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': 'apm-*',
- 'apm_oss.errorIndices': 'apm-*',
- 'apm_oss.onboardingIndices': 'apm-*',
- 'apm_oss.spanIndices': 'apm-*',
- 'apm_oss.transactionIndices': 'apm-*',
- 'apm_oss.metricsIndices': 'apm-*',
- /* eslint-enable @typescript-eslint/naming-convention */
+ sourcemap: 'apm-*',
+ error: 'apm-*',
+ onboarding: 'apm-*',
+ span: 'apm-*',
+ transaction: 'apm-*',
+ metric: 'apm-*',
apmAgentConfigurationIndex: '.apm-agent-configuration',
apmCustomLinkIndex: '.apm-custom-link',
},
diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.test.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.test.ts
index f17224384842d..1fac873ced7be 100644
--- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.test.ts
@@ -58,8 +58,7 @@ describe('getIsUsingTransactionEvents', () => {
describe('with config xpack.apm.searchAggregatedTransactions: never', () => {
const config = {
- 'xpack.apm.searchAggregatedTransactions':
- SearchAggregatedTransactionSetting.never,
+ searchAggregatedTransactions: SearchAggregatedTransactionSetting.never,
};
it('should be false', async () => {
@@ -81,8 +80,7 @@ describe('getIsUsingTransactionEvents', () => {
describe('with config xpack.apm.searchAggregatedTransactions: always', () => {
const config = {
- 'xpack.apm.searchAggregatedTransactions':
- SearchAggregatedTransactionSetting.always,
+ searchAggregatedTransactions: SearchAggregatedTransactionSetting.always,
};
it('should be false when kuery is empty', async () => {
mock = await inspectSearchParams(
@@ -164,8 +162,7 @@ describe('getIsUsingTransactionEvents', () => {
describe('with config xpack.apm.searchAggregatedTransactions: auto', () => {
const config = {
- 'xpack.apm.searchAggregatedTransactions':
- SearchAggregatedTransactionSetting.auto,
+ searchAggregatedTransactions: SearchAggregatedTransactionSetting.auto,
};
it('should query for data once if metrics data found', async () => {
diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.ts
index 70df0959a63b6..66e9697ab7c91 100644
--- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.ts
@@ -23,8 +23,7 @@ export async function getIsUsingTransactionEvents({
start?: number;
end?: number;
}): Promise {
- const searchAggregatedTransactions =
- config['xpack.apm.searchAggregatedTransactions'];
+ const searchAggregatedTransactions = config.searchAggregatedTransactions;
if (
searchAggregatedTransactions === SearchAggregatedTransactionSetting.never
diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts
index 478f3218ef38c..a58a95dd43fcc 100644
--- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts
@@ -63,8 +63,7 @@ export async function getSearchAggregatedTransactions({
apmEventClient: APMEventClient;
kuery: string;
}): Promise {
- const searchAggregatedTransactions =
- config['xpack.apm.searchAggregatedTransactions'];
+ const searchAggregatedTransactions = config.searchAggregatedTransactions;
if (
kuery ||
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.test.ts
index 4983d6d515944..5ef3786e9bde4 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.test.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-/* eslint-disable @typescript-eslint/naming-convention */
import { APMEventESSearchRequest } from '.';
import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices';
import { unpackProcessorEvents } from './unpack_processor_events';
@@ -19,12 +18,12 @@ describe('unpackProcessorEvents', () => {
} as APMEventESSearchRequest;
const indices = {
- 'apm_oss.transactionIndices': 'my-apm-*-transaction-*',
- 'apm_oss.metricsIndices': 'my-apm-*-metric-*',
- 'apm_oss.errorIndices': 'my-apm-*-error-*',
- 'apm_oss.spanIndices': 'my-apm-*-span-*',
- 'apm_oss.onboardingIndices': 'my-apm-*-onboarding-',
- 'apm_oss.sourcemapIndices': 'my-apm-*-sourcemap-*',
+ transaction: 'my-apm-*-transaction-*',
+ metric: 'my-apm-*-metric-*',
+ error: 'my-apm-*-error-*',
+ span: 'my-apm-*-span-*',
+ onboarding: 'my-apm-*-onboarding-*',
+ sourcemap: 'my-apm-*-sourcemap-*',
} as ApmIndicesConfig;
res = unpackProcessorEvents(request, indices);
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts
index 47a2b3fe7e5c8..582fe0374c5ca 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts
@@ -13,19 +13,16 @@ import {
ESFilter,
} from '../../../../../../../../src/core/types/elasticsearch';
import { APMEventESSearchRequest, APMEventESTermsEnumRequest } from '.';
-import {
- ApmIndicesConfig,
- ApmIndicesName,
-} from '../../../settings/apm_indices/get_apm_indices';
+import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices';
-const processorEventIndexMap: Record = {
- [ProcessorEvent.transaction]: 'apm_oss.transactionIndices',
- [ProcessorEvent.span]: 'apm_oss.spanIndices',
- [ProcessorEvent.metric]: 'apm_oss.metricsIndices',
- [ProcessorEvent.error]: 'apm_oss.errorIndices',
+const processorEventIndexMap = {
+ [ProcessorEvent.transaction]: 'transaction',
+ [ProcessorEvent.span]: 'span',
+ [ProcessorEvent.metric]: 'metric',
+ [ProcessorEvent.error]: 'error',
// TODO: should have its own config setting
- [ProcessorEvent.profile]: 'apm_oss.transactionIndices',
-};
+ [ProcessorEvent.profile]: 'transaction',
+} as const;
export function unpackProcessorEvents(
request: APMEventESSearchRequest | APMEventESTermsEnumRequest,
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
index 94e88a09ea35c..d5ff97c050d9d 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
@@ -10,19 +10,20 @@ import { APMConfig } from '../..';
import { APMRouteHandlerResources } from '../../routes/typings';
import { ProcessorEvent } from '../../../common/processor_event';
import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames';
+import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
+import { PromiseReturnType } from '../../../../observability/typings/common';
jest.mock('../settings/apm_indices/get_apm_indices', () => ({
- getApmIndices: async () => ({
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': 'apm-*',
- 'apm_oss.errorIndices': 'apm-*',
- 'apm_oss.onboardingIndices': 'apm-*',
- 'apm_oss.spanIndices': 'apm-*',
- 'apm_oss.transactionIndices': 'apm-*',
- 'apm_oss.metricsIndices': 'apm-*',
- /* eslint-enable @typescript-eslint/naming-convention */
- apmAgentConfigurationIndex: 'apm-*',
- }),
+ getApmIndices: async () =>
+ ({
+ sourcemap: 'apm-*',
+ error: 'apm-*',
+ onboarding: 'apm-*',
+ span: 'apm-*',
+ transaction: 'apm-*',
+ metric: 'apm-*',
+ apmAgentConfigurationIndex: 'apm-*',
+ } as PromiseReturnType),
}));
jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
index aae707c6e4689..83adab6ae6cbc 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-/* eslint-disable @typescript-eslint/naming-convention */
import { createStaticIndexPattern } from './create_static_index_pattern';
import { Setup } from '../helpers/setup_request';
import * as HistoricalAgentData from '../../routes/historical_data/has_historical_agent_data';
@@ -25,11 +24,11 @@ function getMockSavedObjectsClient(existingIndexPatternTitle: string) {
const setup = {
indices: {
- 'apm_oss.transactionIndices': 'apm-*-transaction-*',
- 'apm_oss.spanIndices': 'apm-*-span-*',
- 'apm_oss.errorIndices': 'apm-*-error-*',
- 'apm_oss.metricsIndices': 'apm-*-metrics-*',
- },
+ transaction: 'apm-*-transaction-*',
+ span: 'apm-*-span-*',
+ error: 'apm-*-error-*',
+ metric: 'apm-*-metrics-*',
+ } as APMConfig['indices'],
} as unknown as Setup;
describe('createStaticIndexPattern', () => {
@@ -37,7 +36,7 @@ describe('createStaticIndexPattern', () => {
const savedObjectsClient = getMockSavedObjectsClient('apm-*');
await createStaticIndexPattern({
setup,
- config: { 'xpack.apm.autocreateApmIndexPattern': false } as APMConfig,
+ config: { autocreateApmIndexPattern: false } as APMConfig,
savedObjectsClient,
spaceId: 'default',
});
@@ -54,7 +53,7 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern({
setup,
- config: { 'xpack.apm.autocreateApmIndexPattern': true } as APMConfig,
+ config: { autocreateApmIndexPattern: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
});
@@ -71,7 +70,7 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern({
setup,
- config: { 'xpack.apm.autocreateApmIndexPattern': true } as APMConfig,
+ config: { autocreateApmIndexPattern: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
});
@@ -91,9 +90,7 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern({
setup,
- config: {
- 'xpack.apm.autocreateApmIndexPattern': true,
- } as APMConfig,
+ config: { autocreateApmIndexPattern: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
});
@@ -120,9 +117,7 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern({
setup,
- config: {
- 'xpack.apm.autocreateApmIndexPattern': true,
- } as APMConfig,
+ config: { autocreateApmIndexPattern: true } as APMConfig,
savedObjectsClient,
spaceId: 'default',
});
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
index 4f35e7e639151..26ae2ac337e88 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
@@ -34,7 +34,7 @@ export async function createStaticIndexPattern({
}): Promise {
return withApmSpan('create_static_index_pattern', async () => {
// don't autocreate APM index pattern if it's been disabled via the config
- if (!config['xpack.apm.autocreateApmIndexPattern']) {
+ if (!config.autocreateApmIndexPattern) {
return false;
}
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.test.ts
index 8103630157584..8b7444ffdf6fa 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.test.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.test.ts
@@ -5,18 +5,16 @@
* 2.0.
*/
-/* eslint-disable @typescript-eslint/naming-convention */
-
import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices';
import { getApmIndexPatternTitle } from './get_apm_index_pattern_title';
describe('getApmIndexPatternTitle', () => {
it('returns an index pattern title by combining existing indicies', () => {
const title = getApmIndexPatternTitle({
- 'apm_oss.transactionIndices': 'apm-*-transaction-*',
- 'apm_oss.spanIndices': 'apm-*-span-*',
- 'apm_oss.errorIndices': 'apm-*-error-*',
- 'apm_oss.metricsIndices': 'apm-*-metrics-*',
+ transaction: 'apm-*-transaction-*',
+ span: 'apm-*-span-*',
+ error: 'apm-*-error-*',
+ metric: 'apm-*-metrics-*',
} as ApmIndicesConfig);
expect(title).toBe(
'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*'
@@ -25,10 +23,10 @@ describe('getApmIndexPatternTitle', () => {
it('removes duplicates', () => {
const title = getApmIndexPatternTitle({
- 'apm_oss.transactionIndices': 'apm-*',
- 'apm_oss.spanIndices': 'apm-*',
- 'apm_oss.errorIndices': 'apm-*',
- 'apm_oss.metricsIndices': 'apm-*',
+ transaction: 'apm-*',
+ span: 'apm-*',
+ error: 'apm-*',
+ metric: 'apm-*',
} as ApmIndicesConfig);
expect(title).toBe('apm-*');
});
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
index e65f200130e9a..5e055ff1c2fdc 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
@@ -10,9 +10,9 @@ import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices';
export function getApmIndexPatternTitle(apmIndicesConfig: ApmIndicesConfig) {
return uniq([
- apmIndicesConfig['apm_oss.transactionIndices'],
- apmIndicesConfig['apm_oss.spanIndices'],
- apmIndicesConfig['apm_oss.errorIndices'],
- apmIndicesConfig['apm_oss.metricsIndices'],
+ apmIndicesConfig.transaction,
+ apmIndicesConfig.span,
+ apmIndicesConfig.error,
+ apmIndicesConfig.metric,
]).join(',');
}
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.test.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.test.ts
new file mode 100644
index 0000000000000..c22c326473e2c
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.test.ts
@@ -0,0 +1,133 @@
+/*
+ * 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 {
+ METRIC_JAVA_GC_COUNT,
+ METRIC_JAVA_GC_TIME,
+} from '../../../../../../common/elasticsearch_fieldnames';
+import { Setup } from '../../../../helpers/setup_request';
+import { ChartBase } from '../../../types';
+
+import { fetchAndTransformGcMetrics } from './fetch_and_transform_gc_metrics';
+
+describe('fetchAndTransformGcMetrics', () => {
+ describe('given "jvm.gc.time"', () => {
+ it('converts the value to milliseconds', async () => {
+ const chartBase = {} as unknown as ChartBase;
+ const response = {
+ hits: { total: { value: 1 } },
+ aggregations: {
+ per_pool: {
+ buckets: [
+ {
+ key: 'Copy',
+ doc_count: 30,
+ timeseries: {
+ buckets: [
+ {
+ key_as_string: '2021-10-05T16:03:30.000Z',
+ key: 1633449810000,
+ doc_count: 1,
+ max: {
+ value: 23750,
+ },
+ derivative: {
+ value: 11,
+ },
+ value: {
+ value: 11,
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ };
+ const setup = {
+ apmEventClient: { search: () => Promise.resolve(response) },
+ config: { 'xpack.gc.metricsInterval': 0 },
+ } as unknown as Setup;
+ const fieldName = METRIC_JAVA_GC_TIME;
+
+ const { series } = await fetchAndTransformGcMetrics({
+ chartBase,
+ environment: 'test environment',
+ fieldName,
+ kuery: '',
+ operationName: 'test operation name',
+ setup,
+ serviceName: 'test service name',
+ start: 1633456140000,
+ end: 1633457078105,
+ });
+
+ expect(series[0].data[0].y).toEqual(22000);
+ });
+ });
+
+ describe('given "jvm.gc.rate"', () => {
+ it('does not convert the value to milliseconds', async () => {
+ const chartBase = {} as unknown as ChartBase;
+ const response = {
+ hits: {
+ total: {
+ value: 62,
+ },
+ },
+ aggregations: {
+ per_pool: {
+ buckets: [
+ {
+ key: 'Copy',
+ doc_count: 31,
+ timeseries: {
+ buckets: [
+ {
+ key_as_string: '2021-10-05T18:01:30.000Z',
+ key: 1633456890000,
+ doc_count: 1,
+ max: {
+ value: 815,
+ },
+ derivative: {
+ value: 4,
+ },
+ value: {
+ value: 4,
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ };
+ const setup = {
+ apmEventClient: { search: () => Promise.resolve(response) },
+ config: { 'xpack.gc.metricsInterval': 0 },
+ } as unknown as Setup;
+ const fieldName = METRIC_JAVA_GC_COUNT;
+
+ const { series } = await fetchAndTransformGcMetrics({
+ chartBase,
+ environment: 'test environment',
+ fieldName,
+ kuery: '',
+ operationName: 'test operation name',
+ setup,
+ serviceName: 'test service name',
+ start: 1633456140000,
+ end: 1633457078105,
+ });
+
+ expect(series[0].data[0].y).toEqual(8);
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts
index 8231e4d3c6faa..06138931c004e 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts
@@ -85,7 +85,7 @@ export async function fetchAndTransformGcMetrics({
date_histogram: getMetricsDateHistogramParams({
start,
end,
- metricsInterval: config['xpack.apm.metricsInterval'],
+ metricsInterval: config.metricsInterval,
}),
aggs: {
// get the max value
@@ -135,10 +135,17 @@ export async function fetchAndTransformGcMetrics({
const data = timeseriesData.buckets.map((bucket) => {
// derivative/value will be undefined for the first hit and if the `max` value is null
const bucketValue = bucket.value?.value;
- const y = isFiniteNumber(bucketValue)
+
+ const unconvertedY = isFiniteNumber(bucketValue)
? round(bucketValue * (60 / bucketSize), 1)
: null;
+ // convert to milliseconds if we're calculating time, but not for rate
+ const y =
+ unconvertedY !== null && fieldName === METRIC_JAVA_GC_TIME
+ ? unconvertedY * 1000
+ : unconvertedY;
+
return {
y,
x: bucket.key,
diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts
index a3fce0368f4a5..581a0782e4d72 100644
--- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts
@@ -99,7 +99,7 @@ export async function fetchAndTransformMetrics({
date_histogram: getMetricsDateHistogramParams({
start,
end,
- metricsInterval: config['xpack.apm.metricsInterval'],
+ metricsInterval: config.metricsInterval,
}),
aggs,
},
diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
index 9409e94fa9ba9..ba35ac5c5c89c 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts
@@ -56,7 +56,7 @@ export async function hasRumData({
const response = await apmEventClient.search('has_rum_data', params);
return {
- indices: setup.indices['apm_oss.transactionIndices']!,
+ indices: setup.indices.transaction,
hasData: response.hits.total.value > 0,
serviceName:
response.aggregations?.services?.mostTraffic?.buckets?.[0]?.key,
@@ -65,7 +65,7 @@ export async function hasRumData({
return {
hasData: false,
serviceName: undefined,
- indices: setup.indices['apm_oss.transactionIndices']!,
+ indices: setup.indices.transaction,
};
}
}
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts
index 02ba0a8514b62..239cf39f15ffe 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts
@@ -65,7 +65,7 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact
const params: FailedTransactionsCorrelationsRequestParams &
SearchStrategyServerParams = {
...searchServiceParams,
- index: indices['apm_oss.transactionIndices'],
+ index: indices.transaction,
includeFrozen,
};
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts
index 7e420c821a746..91f4a0d3349a4 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts
@@ -67,7 +67,7 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch
const indices = await getApmIndices();
params = {
...searchServiceParams,
- index: indices['apm_oss.transactionIndices'],
+ index: indices.transaction,
includeFrozen,
};
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts
index 6e03c879f9b97..8a9d04df32036 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts
@@ -90,10 +90,7 @@ const clientSearchMock = (
};
const getApmIndicesMock = async () =>
- ({
- // eslint-disable-next-line @typescript-eslint/naming-convention
- 'apm_oss.transactionIndices': 'apm-*',
- } as ApmIndicesConfig);
+ ({ transaction: 'apm-*' } as ApmIndicesConfig);
describe('APM Correlations search strategy', () => {
describe('strategy interface', () => {
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
index 2497a85c0c774..ae511d0fed8f8 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
@@ -53,10 +53,7 @@ async function getConnectionData({
end,
});
- const chunks = chunk(
- traceIds,
- setup.config['xpack.apm.serviceMapMaxTracesPerRequest']
- );
+ const chunks = chunk(traceIds, setup.config.serviceMapMaxTracesPerRequest);
const init = {
connections: [],
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts
index 2129606e69fc3..afb88189a5fd2 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts
@@ -70,9 +70,7 @@ describe('getServiceMapServiceNodeInfo', () => {
indices: {},
start: 1593460053026000,
end: 1593497863217000,
- config: {
- 'xpack.apm.metricsInterval': 30,
- },
+ config: { metricsInterval: 30 },
uiFilters: { environment: 'test environment' },
} as unknown as Setup;
const serviceName = 'test service name';
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
index c1c11f7bf639a..7e16e69498e7c 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
@@ -60,13 +60,11 @@ export async function getTraceSampleIds({
query.bool.filter.push(...environmentQuery(environment));
const fingerprintBucketSize = serviceName
- ? config['xpack.apm.serviceMapFingerprintBucketSize']
- : config['xpack.apm.serviceMapFingerprintGlobalBucketSize'];
-
+ ? config.serviceMapFingerprintBucketSize
+ : config.serviceMapFingerprintGlobalBucketSize;
const traceIdBucketSize = serviceName
- ? config['xpack.apm.serviceMapTraceIdBucketSize']
- : config['xpack.apm.serviceMapTraceIdGlobalBucketSize'];
-
+ ? config.serviceMapTraceIdBucketSize
+ : config.serviceMapTraceIdGlobalBucketSize;
const samplerShardSize = traceIdBucketSize * 10;
const params = {
@@ -137,8 +135,7 @@ export async function getTraceSampleIds({
'get_trace_sample_ids',
params
);
- // make sure at least one trace per composite/connection bucket
- // is queried
+ // make sure at least one trace per composite/connection bucket is queried
const traceIdsWithPriority =
tracesSampleResponse.aggregations?.connections.buckets.flatMap((bucket) =>
bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts
index a51f4c4e0fb7d..089282d6f1c34 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts
@@ -17,6 +17,7 @@ import { Coordinate } from '../../../../typings/timeseries';
import { kqlQuery, rangeQuery } from '../../../../../observability/server';
import { environmentQuery } from '../../../../common/utils/environment_query';
import {
+ getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} from '../../helpers/aggregated_transactions';
@@ -111,6 +112,9 @@ export async function getServiceInstancesTransactionStatistics<
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
+ ...getDocumentTypeFilterForAggregatedTransactions(
+ searchAggregatedTransactions
+ ),
...(isComparisonSearch && serviceNodeIds
? [{ terms: { [SERVICE_NODE_NAME]: serviceNodeIds } }]
: []),
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts
index d5a2006060395..fbc1e2880495b 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts
@@ -57,7 +57,7 @@ export async function getServiceTransactionGroups({
end: number;
}) {
const { apmEventClient, config } = setup;
- const bucketSize = config['xpack.apm.ui.transactionGroupBucketSize'];
+ const bucketSize = config.ui.transactionGroupBucketSize;
const field = getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
index 0ade96682b362..107493af1a0c0 100644
--- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
+++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
@@ -5,8 +5,6 @@
* 2.0.
*/
-import { merge } from 'lodash';
-
import { SavedObjectsClient } from 'src/core/server';
import { PromiseReturnType } from '../../../../../observability/typings/common';
import {
@@ -22,8 +20,6 @@ export { ApmIndicesConfig };
type ISavedObjectsClient = Pick;
-export type ApmIndicesName = keyof ApmIndicesConfig;
-
async function getApmIndicesSavedObject(
savedObjectsClient: ISavedObjectsClient
) {
@@ -38,14 +34,12 @@ async function getApmIndicesSavedObject(
export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig {
return {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': config['apm_oss.sourcemapIndices'],
- 'apm_oss.errorIndices': config['apm_oss.errorIndices'],
- 'apm_oss.onboardingIndices': config['apm_oss.onboardingIndices'],
- 'apm_oss.spanIndices': config['apm_oss.spanIndices'],
- 'apm_oss.transactionIndices': config['apm_oss.transactionIndices'],
- 'apm_oss.metricsIndices': config['apm_oss.metricsIndices'],
- /* eslint-enable @typescript-eslint/naming-convention */
+ sourcemap: config.indices.sourcemap,
+ error: config.indices.error,
+ onboarding: config.indices.onboarding,
+ span: config.indices.span,
+ transaction: config.indices.transaction,
+ metric: config.indices.metric,
// system indices, not configurable
apmAgentConfigurationIndex: '.apm-agent-configuration',
apmCustomLinkIndex: '.apm-custom-link',
@@ -64,21 +58,12 @@ export async function getApmIndices({
savedObjectsClient
);
const apmIndicesConfig = getApmIndicesConfig(config);
- return merge({}, apmIndicesConfig, apmIndicesSavedObject);
+ return { ...apmIndicesConfig, ...apmIndicesSavedObject };
} catch (error) {
return getApmIndicesConfig(config);
}
}
-const APM_UI_INDICES: ApmIndicesName[] = [
- 'apm_oss.sourcemapIndices',
- 'apm_oss.errorIndices',
- 'apm_oss.onboardingIndices',
- 'apm_oss.spanIndices',
- 'apm_oss.transactionIndices',
- 'apm_oss.metricsIndices',
-];
-
export async function getApmIndexSettings({
context,
config,
@@ -88,7 +73,7 @@ export async function getApmIndexSettings({
apmIndicesSavedObject = await getApmIndicesSavedObject(
context.core.savedObjects.client
);
- } catch (error) {
+ } catch (error: any) {
if (error.output && error.output.statusCode === 404) {
apmIndicesSavedObject = {};
} else {
@@ -97,7 +82,11 @@ export async function getApmIndexSettings({
}
const apmIndicesConfig = getApmIndicesConfig(config);
- return APM_UI_INDICES.map((configurationName) => ({
+ const apmIndices = Object.keys(config.indices) as Array<
+ keyof typeof config.indices
+ >;
+
+ return apmIndices.map((configurationName) => ({
configurationName,
defaultValue: apmIndicesConfig[configurationName], // value defined in kibana[.dev].yml
savedValue: apmIndicesSavedObject[configurationName], // value saved via Saved Objects service
diff --git a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap
index 53318fe5fe594..ea8f39f9d9b6d 100644
--- a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap
@@ -37,7 +37,7 @@ Object {
},
},
},
- "size": "myIndex",
+ "size": 1000,
},
}
`;
diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts
index e940100edcf52..60a28fd9abdbd 100644
--- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts
+++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts
@@ -24,7 +24,7 @@ export async function getTraceItems(
end: number
) {
const { apmEventClient, config } = setup;
- const maxTraceItems = config['xpack.apm.ui.maxTraceItems'];
+ const maxTraceItems = config.ui.maxTraceItems;
const excludedLogLevels = ['debug', 'info', 'warning'];
const errorResponsePromise = apmEventClient.search('get_errors_docs', {
@@ -80,9 +80,5 @@ export async function getTraceItems(
const traceDocs = traceResponse.hits.hits.map((hit) => hit._source);
const errorDocs = errorResponse.hits.hits.map((hit) => hit._source);
- return {
- exceedsMax,
- traceDocs,
- errorDocs,
- };
+ return { exceedsMax, traceDocs, errorDocs };
}
diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts
index 6e9d0aad96b71..76cabd3e3af93 100644
--- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts
@@ -11,16 +11,15 @@ import noDataResponse from './mock_responses/no_data.json';
import dataResponse from './mock_responses/data.json';
import { APMConfig } from '../../..';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
-
-const mockIndices = {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': 'myIndex',
- 'apm_oss.errorIndices': 'myIndex',
- 'apm_oss.onboardingIndices': 'myIndex',
- 'apm_oss.spanIndices': 'myIndex',
- 'apm_oss.transactionIndices': 'myIndex',
- 'apm_oss.metricsIndices': 'myIndex',
- /* eslint-enable @typescript-eslint/naming-convention */
+import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
+
+const mockIndices: ApmIndicesConfig = {
+ sourcemap: 'myIndex',
+ error: 'myIndex',
+ onboarding: 'myIndex',
+ span: 'myIndex',
+ transaction: 'myIndex',
+ metric: 'myIndex',
apmAgentConfigurationIndex: 'myIndex',
apmCustomLinkIndex: 'myIndex',
};
diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
index 62277dba8ac29..a5c11776c70b0 100644
--- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
@@ -124,7 +124,7 @@ export async function getTransactionBreakdown({
date_histogram: getMetricsDateHistogramParams({
start,
end,
- metricsInterval: config['xpack.apm.metricsInterval'],
+ metricsInterval: config.metricsInterval,
}),
aggs: subAggs,
},
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index 2296227de2a33..d2d8dbf602364 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -5,8 +5,7 @@
* 2.0.
*/
-import { combineLatest } from 'rxjs';
-import { map, take } from 'rxjs/operators';
+import { take } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
@@ -19,8 +18,7 @@ import { isEmpty, mapValues } from 'lodash';
import { SavedObjectsClient } from '../../../../src/core/server';
import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map';
import { Dataset } from '../../rule_registry/server';
-import { APMConfig, APMXPackConfig, APM_SERVER_FEATURE_ID } from '.';
-import { mergeConfigs } from './index';
+import { APMConfig, APM_SERVER_FEATURE_ID } from '.';
import { UI_SETTINGS } from '../../../../src/plugins/data/common';
import { APM_FEATURE, registerFeaturesUsage } from './feature';
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
@@ -73,29 +71,23 @@ export class APMPlugin
plugins: Omit
) {
this.logger = this.initContext.logger.get();
- const config$ = this.initContext.config.create();
- const mergedConfig$ = combineLatest(plugins.apmOss.config$, config$).pipe(
- map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig))
- );
+ const config$ = this.initContext.config.create();
core.savedObjects.registerType(apmIndices);
core.savedObjects.registerType(apmTelemetry);
core.savedObjects.registerType(apmServerSettings);
- const currentConfig = mergeConfigs(
- plugins.apmOss.config,
- this.initContext.config.get()
- );
+ const currentConfig = this.initContext.config.get();
this.currentConfig = currentConfig;
if (
plugins.taskManager &&
plugins.usageCollection &&
- currentConfig['xpack.apm.telemetryCollectionEnabled']
+ currentConfig.telemetryCollectionEnabled
) {
createApmTelemetry({
core,
- config$: mergedConfig$,
+ config$,
usageCollector: plugins.usageCollection,
taskManager: plugins.taskManager,
logger: this.logger,
@@ -156,7 +148,7 @@ export class APMPlugin
const boundGetApmIndices = async () =>
getApmIndices({
savedObjectsClient: await getInternalSavedObjectsClient(core),
- config: await mergedConfig$.pipe(take(1)).toPromise(),
+ config: await config$.pipe(take(1)).toPromise(),
});
boundGetApmIndices().then((indices) => {
@@ -193,7 +185,7 @@ export class APMPlugin
ruleDataClient,
alerting: plugins.alerting,
ml: plugins.ml,
- config$: mergedConfig$,
+ config$,
logger: this.logger!.get('rule'),
});
}
@@ -231,7 +223,7 @@ export class APMPlugin
});
return {
- config$: mergedConfig$,
+ config$,
getApmIndices: boundGetApmIndices,
createApmEventClient: async ({
request,
diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts
index d8097228df0dc..2884c08ceb9a1 100644
--- a/x-pack/plugins/apm/server/routes/fleet.ts
+++ b/x-pack/plugins/apm/server/routes/fleet.ts
@@ -129,8 +129,7 @@ const getMigrationCheckRoute = createApmServerRoute({
options: { tags: ['access:apm'] },
handler: async (resources) => {
const { plugins, context, config, request } = resources;
- const cloudApmMigrationEnabled =
- config['xpack.apm.agent.migrations.enabled'];
+ const cloudApmMigrationEnabled = config.agent.migrations.enabled;
if (!plugins.fleet || !plugins.security) {
throw Boom.internal(FLEET_SECURITY_REQUIRED_MESSAGE);
}
@@ -158,8 +157,7 @@ const createCloudApmPackagePolicyRoute = createApmServerRoute({
options: { tags: ['access:apm', 'access:apm_write'] },
handler: async (resources) => {
const { plugins, context, config, request, logger } = resources;
- const cloudApmMigrationEnabled =
- config['xpack.apm.agent.migrations.enabled'];
+ const cloudApmMigrationEnabled = config.agent.migrations.enabled;
if (!plugins.fleet || !plugins.security) {
throw Boom.internal(FLEET_SECURITY_REQUIRED_MESSAGE);
}
diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts
index f9062ac13e049..17fb9d7c98c5f 100644
--- a/x-pack/plugins/apm/server/routes/service_map.ts
+++ b/x-pack/plugins/apm/server/routes/service_map.ts
@@ -33,7 +33,7 @@ const serviceMapRoute = createApmServerRoute({
options: { tags: ['access:apm'] },
handler: async (resources) => {
const { config, context, params, logger } = resources;
- if (!config['xpack.apm.serviceMapEnabled']) {
+ if (!config.serviceMapEnabled) {
throw Boom.notFound();
}
if (!isActivePlatinumLicense(context.licensing.license)) {
@@ -81,7 +81,7 @@ const serviceMapServiceNodeRoute = createApmServerRoute({
handler: async (resources) => {
const { config, context, params } = resources;
- if (!config['xpack.apm.serviceMapEnabled']) {
+ if (!config.serviceMapEnabled) {
throw Boom.notFound();
}
if (!isActivePlatinumLicense(context.licensing.license)) {
@@ -125,7 +125,7 @@ const serviceMapBackendNodeRoute = createApmServerRoute({
handler: async (resources) => {
const { config, context, params } = resources;
- if (!config['xpack.apm.serviceMapEnabled']) {
+ if (!config.serviceMapEnabled) {
throw Boom.notFound();
}
if (!isActivePlatinumLicense(context.licensing.license)) {
diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
index 1cba5f972c27e..156f4d1af0bb2 100644
--- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
+++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
@@ -13,6 +13,7 @@ import {
getApmIndexSettings,
} from '../../lib/settings/apm_indices/get_apm_indices';
import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices';
+import { APMConfig } from '../..';
// get list of apm indices and values
const apmIndexSettingsRoute = createApmServerRoute({
@@ -37,6 +38,10 @@ const apmIndicesRoute = createApmServerRoute({
},
});
+type SaveApmIndicesBodySchema = {
+ [Property in keyof APMConfig['indices']]: t.StringC;
+};
+
// save ui indices
const saveApmIndicesRoute = createApmServerRoute({
endpoint: 'POST /internal/apm/settings/apm-indices/save',
@@ -45,15 +50,13 @@ const saveApmIndicesRoute = createApmServerRoute({
},
params: t.type({
body: t.partial({
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': t.string,
- 'apm_oss.errorIndices': t.string,
- 'apm_oss.onboardingIndices': t.string,
- 'apm_oss.spanIndices': t.string,
- 'apm_oss.transactionIndices': t.string,
- 'apm_oss.metricsIndices': t.string,
- /* eslint-enable @typescript-eslint/naming-convention */
- }),
+ sourcemap: t.string,
+ error: t.string,
+ onboarding: t.string,
+ span: t.string,
+ transaction: t.string,
+ metric: t.string,
+ } as SaveApmIndicesBodySchema),
}),
handler: async (resources) => {
const { params, context } = resources;
diff --git a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts
index df5267023ae89..4aa6c4953056a 100644
--- a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts
+++ b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts
@@ -7,34 +7,24 @@
import { SavedObjectsType } from 'src/core/server';
import { i18n } from '@kbn/i18n';
+import { updateApmOssIndexPaths } from './migrations/update_apm_oss_index_paths';
+import { ApmIndicesConfigName } from '..';
+
+const properties: { [Property in ApmIndicesConfigName]: { type: 'keyword' } } =
+ {
+ sourcemap: { type: 'keyword' },
+ error: { type: 'keyword' },
+ onboarding: { type: 'keyword' },
+ span: { type: 'keyword' },
+ transaction: { type: 'keyword' },
+ metric: { type: 'keyword' },
+ };
export const apmIndices: SavedObjectsType = {
name: 'apm-indices',
hidden: false,
namespaceType: 'agnostic',
- mappings: {
- properties: {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': {
- type: 'keyword',
- },
- 'apm_oss.errorIndices': {
- type: 'keyword',
- },
- 'apm_oss.onboardingIndices': {
- type: 'keyword',
- },
- 'apm_oss.spanIndices': {
- type: 'keyword',
- },
- 'apm_oss.transactionIndices': {
- type: 'keyword',
- },
- 'apm_oss.metricsIndices': {
- type: 'keyword',
- },
- },
- },
+ mappings: { properties },
management: {
importableAndExportable: true,
icon: 'apmApp',
@@ -43,4 +33,10 @@ export const apmIndices: SavedObjectsType = {
defaultMessage: 'APM Settings - Index',
}),
},
+ migrations: {
+ '7.16.0': (doc) => {
+ const attributes = updateApmOssIndexPaths(doc.attributes);
+ return { ...doc, attributes };
+ },
+ },
};
diff --git a/x-pack/plugins/apm/server/saved_objects/migrations/update_apm_oss_index_paths.ts b/x-pack/plugins/apm/server/saved_objects/migrations/update_apm_oss_index_paths.ts
new file mode 100644
index 0000000000000..72ba40db0ce05
--- /dev/null
+++ b/x-pack/plugins/apm/server/saved_objects/migrations/update_apm_oss_index_paths.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 apmIndexConfigs = [
+ ['sourcemap', 'apm_oss.sourcemapIndices'],
+ ['error', 'apm_oss.errorIndices'],
+ ['onboarding', 'apm_oss.onboardingIndices'],
+ ['span', 'apm_oss.spanIndices'],
+ ['transaction', 'apm_oss.transactionIndices'],
+ ['metric', 'apm_oss.metricsIndices'],
+] as const;
+
+type ApmIndexConfigs = typeof apmIndexConfigs[number][0];
+type ApmIndicesSavedObjectAttributes = Partial<{
+ [Property in ApmIndexConfigs]: string;
+}>;
+type DeprecatedApmIndexConfigPaths = typeof apmIndexConfigs[number][1];
+type DeprecatedApmIndicesSavedObjectAttributes = Partial<{
+ [Property in DeprecatedApmIndexConfigPaths]: string;
+}>;
+
+export function updateApmOssIndexPaths(
+ attributes: DeprecatedApmIndicesSavedObjectAttributes
+) {
+ return apmIndexConfigs.reduce((attrs, [configPath, deprecatedConfigPath]) => {
+ const indexConfig: string | undefined = attributes[deprecatedConfigPath];
+ if (indexConfig) {
+ attrs[configPath] = indexConfig;
+ }
+ return attrs;
+ }, {} as ApmIndicesSavedObjectAttributes);
+}
diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts
index fb9fbae33ac82..ba99b0624c441 100644
--- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts
+++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts
@@ -37,13 +37,7 @@ export function onPremInstructions({
apmConfig,
isFleetPluginEnabled,
}: {
- apmConfig: Pick<
- APMConfig,
- | 'apm_oss.errorIndices'
- | 'apm_oss.transactionIndices'
- | 'apm_oss.metricsIndices'
- | 'apm_oss.onboardingIndices'
- >;
+ apmConfig: APMConfig;
isFleetPluginEnabled: boolean;
}): InstructionsSchema {
const EDIT_CONFIG = createEditConfig();
@@ -76,7 +70,12 @@ export function onPremInstructions({
{
id: INSTRUCTION_VARIANT.FLEET,
instructions: [
- { customComponentName: 'TutorialFleetInstructions' },
+ {
+ title: i18n.translate('xpack.apm.tutorial.fleet.title', {
+ defaultMessage: 'Fleet',
+ }),
+ customComponentName: 'TutorialFleetInstructions',
+ },
],
},
]
@@ -144,7 +143,7 @@ export function onPremInstructions({
}
),
esHitsCheck: {
- index: apmConfig['apm_oss.onboardingIndices'],
+ index: apmConfig.indices.onboarding,
query: {
bool: {
filter: [
@@ -237,9 +236,9 @@ export function onPremInstructions({
),
esHitsCheck: {
index: [
- apmConfig['apm_oss.errorIndices'],
- apmConfig['apm_oss.transactionIndices'],
- apmConfig['apm_oss.metricsIndices'],
+ apmConfig.indices.error,
+ apmConfig.indices.transaction,
+ apmConfig.indices.metric,
],
query: {
bool: {
diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts
index 66e6ffaed95a8..5caf2b4372483 100644
--- a/x-pack/plugins/apm/server/tutorial/index.ts
+++ b/x-pack/plugins/apm/server/tutorial/index.ts
@@ -67,7 +67,7 @@ export const tutorialProvider =
],
};
- if (apmConfig['xpack.apm.ui.enabled']) {
+ if (apmConfig.ui.enabled) {
// @ts-expect-error artifacts.application is readonly
artifacts.application = {
path: '/app/apm',
diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts
index 325891d8c1d33..c686c42beb6ef 100644
--- a/x-pack/plugins/apm/server/types.ts
+++ b/x-pack/plugins/apm/server/types.ts
@@ -16,7 +16,6 @@ import {
PluginStart as DataPluginStart,
} from '../../../../src/plugins/data/server';
import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
-import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
import {
HomeServerPluginSetup,
HomeServerPluginStart,
@@ -71,10 +70,6 @@ interface DependencyMap {
setup: SpacesPluginSetup;
start: SpacesPluginStart;
};
- apmOss: {
- setup: APMOSSPluginSetup;
- start: undefined;
- };
home: {
setup: HomeServerPluginSetup;
start: HomeServerPluginStart;
@@ -135,7 +130,6 @@ interface DependencyMap {
const requiredDependencies = [
'features',
- 'apmOss',
'data',
'licensing',
'triggersActionsUi',
diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx
index 7b6b549e07c8d..5cf5016aec2e9 100644
--- a/x-pack/plugins/apm/server/utils/test_helpers.tsx
+++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx
@@ -12,6 +12,7 @@ import {
ESSearchResponse,
} from '../../../../../src/core/types/elasticsearch';
import { UxUIFilters } from '../../typings/ui_filters';
+import { ApmIndicesConfig } from '../lib/settings/apm_indices/get_apm_indices';
interface Options {
mockResponse?: (
@@ -26,18 +27,7 @@ interface MockSetup {
internalClient: any;
config: APMConfig;
uiFilters: UxUIFilters;
- indices: {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': string;
- 'apm_oss.errorIndices': string;
- 'apm_oss.onboardingIndices': string;
- 'apm_oss.spanIndices': string;
- 'apm_oss.transactionIndices': string;
- 'apm_oss.metricsIndices': string;
- /* eslint-enable @typescript-eslint/naming-convention */
- apmAgentConfigurationIndex: string;
- apmCustomLinkIndex: string;
- };
+ indices: ApmIndicesConfig;
}
export async function inspectSearchParams(
@@ -61,6 +51,16 @@ export async function inspectSearchParams(
let response;
let error;
+ const mockApmIndices: {
+ [Property in keyof APMConfig['indices']]: string;
+ } = {
+ sourcemap: 'myIndex',
+ error: 'myIndex',
+ onboarding: 'myIndex',
+ span: 'myIndex',
+ transaction: 'myIndex',
+ metric: 'myIndex',
+ };
const mockSetup = {
apmEventClient: { search: spy } as any,
internalClient: { search: spy } as any,
@@ -76,8 +76,15 @@ export async function inspectSearchParams(
switch (key) {
default:
return 'myIndex';
-
- case 'xpack.apm.metricsInterval':
+ case 'indices':
+ return mockApmIndices;
+ case 'ui':
+ return {
+ enabled: true,
+ transactionGroupBucketSize: 1000,
+ maxTraceItems: 1000,
+ };
+ case 'metricsInterval':
return 30;
}
},
@@ -85,14 +92,7 @@ export async function inspectSearchParams(
) as APMConfig,
uiFilters: options?.uiFilters ?? {},
indices: {
- /* eslint-disable @typescript-eslint/naming-convention */
- 'apm_oss.sourcemapIndices': 'myIndex',
- 'apm_oss.errorIndices': 'myIndex',
- 'apm_oss.onboardingIndices': 'myIndex',
- 'apm_oss.spanIndices': 'myIndex',
- 'apm_oss.transactionIndices': 'myIndex',
- 'apm_oss.metricsIndices': 'myIndex',
- /* eslint-enable @typescript-eslint/naming-convention */
+ ...mockApmIndices,
apmAgentConfigurationIndex: 'myIndex',
apmCustomLinkIndex: 'myIndex',
},
diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json
index c1030d2a4be1d..5db20725dd785 100644
--- a/x-pack/plugins/apm/tsconfig.json
+++ b/x-pack/plugins/apm/tsconfig.json
@@ -19,7 +19,6 @@
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
- { "path": "../../../src/plugins/apm_oss/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
{ "path": "../../../src/plugins/home/tsconfig.json" },
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot
index 6ef6d19e446db..45b9d896db5f2 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot
@@ -54,7 +54,7 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = `
>
@@ -118,7 +118,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = `
/>
Select all rows
@@ -166,7 +166,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = `
@@ -921,7 +921,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = `
className="euiPagination__item"
>
;
-
storiesOf('components/WorkpadHeader/ElementMenu', module).add('default', () => (
-
+
));
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx
index 937912570b77f..8ac581b0866a4 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx
@@ -6,17 +6,13 @@
*/
import { sortBy } from 'lodash';
-import React, { Fragment, FunctionComponent, useState } from 'react';
+import React, { FunctionComponent, useState } from 'react';
import PropTypes from 'prop-types';
-import {
- EuiButton,
- EuiContextMenu,
- EuiIcon,
- EuiContextMenuPanelItemDescriptor,
-} from '@elastic/eui';
+import { EuiContextMenu, EuiIcon, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { PrimaryActionPopover } from '../../../../../../../src/plugins/presentation_util/public';
import { getId } from '../../../lib/get_id';
-import { Popover, ClosePopoverFn } from '../../popover';
+import { ClosePopoverFn } from '../../popover';
import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib';
import { ElementSpec } from '../../../../types';
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
@@ -116,7 +112,7 @@ const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: Ele
return categories;
};
-export interface Props {
+interface Props {
/**
* Dictionary of elements from elements registry
*/
@@ -125,25 +121,14 @@ export interface Props {
* Handler for adding a selected element to the workpad
*/
addElement: (element: ElementSpec) => void;
- /**
- * Renders embeddable flyout
- */
- renderEmbedPanel: (onClose: () => void) => JSX.Element;
}
-export const ElementMenu: FunctionComponent = ({
- elements,
- addElement,
- renderEmbedPanel,
-}) => {
+export const ElementMenu: FunctionComponent = ({ elements, addElement }) => {
const [isAssetModalVisible, setAssetModalVisible] = useState(false);
- const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false);
const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false);
const hideAssetModal = () => setAssetModalVisible(false);
const showAssetModal = () => setAssetModalVisible(true);
- const hideEmbedPanel = () => setEmbedPanelVisible(false);
- const showEmbedPanel = () => setEmbedPanelVisible(true);
const hideSavedElementsModal = () => setSavedElementsModalVisible(false);
const showSavedElementsModal = () => setSavedElementsModalVisible(true);
@@ -214,47 +199,28 @@ export const ElementMenu: FunctionComponent = ({
closePopover();
},
},
- {
- name: strings.getEmbedObjectMenuItemLabel(),
- className: CONTEXT_MENU_TOP_BORDER_CLASSNAME,
- icon: ,
- onClick: () => {
- showEmbedPanel();
- closePopover();
- },
- },
],
};
};
- const exportControl = (togglePopover: React.MouseEventHandler) => (
-