diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts
index 83f1ecb2a759dd..19fed0e78885fc 100644
--- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts
+++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts
@@ -16,7 +16,6 @@ const STORYBOOKS = [
'canvas',
'cases',
'cell_actions',
- 'ci_composite',
'cloud_chat',
'coloring',
'chart_icons',
@@ -93,14 +92,12 @@ const upload = () => {
console.log('--- Generating Storybooks HTML');
process.chdir(path.join('.', 'built_assets', 'storybook'));
- fs.renameSync('ci_composite', 'composite');
const storybooks = execSync(`ls -1d */`)
.toString()
.trim()
.split('\n')
- .map((filePath) => filePath.replace('/', ''))
- .filter((filePath) => filePath !== 'composite');
+ .map((filePath) => filePath.replace('/', ''));
const listHtml = storybooks
.map((storybook) => `
${storybook}`)
@@ -110,8 +107,6 @@ const upload = () => {
Storybooks
- Composite Storybook
- All
diff --git a/.ci/.storybook/main.js b/.ci/.storybook/main.js
deleted file mode 100644
index c4e017179021a0..00000000000000
--- a/.ci/.storybook/main.js
+++ /dev/null
@@ -1,34 +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.
- */
-
-const config = require('@kbn/storybook').defaultConfig;
-const aliases = require('../../src/dev/storybook/aliases').storybookAliases;
-
-config.refs = {};
-
-// Required due to https://github.com/storybookjs/storybook/issues/13834
-config.babel = async (options) => ({
- ...options,
- plugins: ['@babel/plugin-transform-typescript', ...options.plugins],
-});
-
-for (const alias of Object.keys(aliases).filter((a) => a !== 'ci_composite')) {
- // snake_case -> Title Case
- const title = alias
- .replace(/_/g, ' ')
- .split(' ')
- .map((n) => n[0].toUpperCase() + n.slice(1))
- .join(' ');
-
- config.refs[alias] = {
- title: title,
- url: `${process.env.STORYBOOK_BASE_URL}/${alias}`,
- };
-}
-
-module.exports = config;
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 994b92442d00d3..70affc4d4b4005 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -536,6 +536,7 @@ x-pack/packages/ml/route_utils @elastic/ml-ui
x-pack/packages/ml/runtime_field_utils @elastic/ml-ui
x-pack/packages/ml/string_hash @elastic/ml-ui
x-pack/packages/ml/trained_models_utils @elastic/ml-ui
+x-pack/packages/ml/ui_actions @elastic/ml-ui
x-pack/packages/ml/url_state @elastic/ml-ui
packages/kbn-monaco @elastic/appex-sharedux
x-pack/plugins/monitoring_collection @elastic/obs-ux-infra_services-team
diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc
index 0bf407ac1ff900..8dc3d8b6e9e0d5 100644
--- a/docs/user/plugins.asciidoc
+++ b/docs/user/plugins.asciidoc
@@ -81,7 +81,6 @@ Use it to create, edit and embed visualizations, and also to search inside an em
* https://github.com/sw-jung/kibana_markdown_doc_view[Markdown Doc View] (sw-jung) - A plugin for custom doc view using markdown+handlebars template.
* https://github.com/datasweet-fr/kibana-datasweet-formula[Datasweet Formula] (datasweet) - enables calculated metric on any standard Kibana visualization.
-* https://github.com/pjhampton/kibana-prometheus-exporter[Prometheus Exporter] - exports the Kibana metrics in the prometheus format
NOTE: To add your plugin to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request].
@@ -183,4 +182,4 @@ you must specify the path to that configuration file each time you use the `bin/
0:: Success
64:: Unknown command or incorrect option parameter
74:: I/O error
-70:: Other error
+70:: Other error
\ No newline at end of file
diff --git a/package.json b/package.json
index 0f8260566b536f..73ae2cc3885434 100644
--- a/package.json
+++ b/package.json
@@ -555,6 +555,7 @@
"@kbn/ml-runtime-field-utils": "link:x-pack/packages/ml/runtime_field_utils",
"@kbn/ml-string-hash": "link:x-pack/packages/ml/string_hash",
"@kbn/ml-trained-models-utils": "link:x-pack/packages/ml/trained_models_utils",
+ "@kbn/ml-ui-actions": "link:x-pack/packages/ml/ui_actions",
"@kbn/ml-url-state": "link:x-pack/packages/ml/url_state",
"@kbn/monaco": "link:packages/kbn-monaco",
"@kbn/monitoring-collection-plugin": "link:x-pack/plugins/monitoring_collection",
@@ -1469,7 +1470,7 @@
"blob-polyfill": "^7.0.20220408",
"callsites": "^3.1.0",
"chance": "1.0.18",
- "chromedriver": "^119.0.0",
+ "chromedriver": "^119.0.1",
"clean-webpack-plugin": "^3.0.0",
"cli-table3": "^0.6.1",
"copy-webpack-plugin": "^6.0.2",
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.test.ts
new file mode 100644
index 00000000000000..3faeee08048ef4
--- /dev/null
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/apis/bulk_create.test.ts
@@ -0,0 +1,1048 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/* eslint-disable @typescript-eslint/no-shadow */
+
+import {
+ pointInTimeFinderMock,
+ mockGetBulkOperationError,
+ mockGetCurrentTime,
+ mockPreflightCheckForCreate,
+ mockGetSearchDsl,
+} from '../repository.test.mock';
+
+import type { Payload } from '@hapi/boom';
+
+import type { SavedObjectsBulkCreateObject } from '@kbn/core-saved-objects-api-server';
+import {
+ type SavedObjectsRawDoc,
+ type SavedObjectUnsanitizedDoc,
+ type SavedObjectReference,
+} from '@kbn/core-saved-objects-server';
+import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server';
+import { SavedObjectsRepository } from '../repository';
+import { loggerMock } from '@kbn/logging-mocks';
+import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal';
+import { kibanaMigratorMock } from '../../mocks';
+import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+
+import {
+ CUSTOM_INDEX_TYPE,
+ NAMESPACE_AGNOSTIC_TYPE,
+ MULTI_NAMESPACE_TYPE,
+ MULTI_NAMESPACE_ISOLATED_TYPE,
+ HIDDEN_TYPE,
+ mockVersionProps,
+ mockTimestampFields,
+ mockTimestamp,
+ mappings,
+ mockVersion,
+ createRegistry,
+ createDocumentMigrator,
+ createSpySerializer,
+ bulkCreateSuccess,
+ getMockBulkCreateResponse,
+ expectErrorResult,
+ expectErrorInvalidType,
+ expectErrorConflict,
+ expectError,
+ createBadRequestErrorPayload,
+ expectCreateResult,
+ mockTimestampFieldsWithCreated,
+} from '../../test_helpers/repository.test.common';
+
+// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
+// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
+
+interface ExpectedErrorResult {
+ type: string;
+ id: string;
+ error: Record;
+}
+
+describe('SavedObjectsRepository', () => {
+ let client: ReturnType;
+ let repository: SavedObjectsRepository;
+ let migrator: ReturnType;
+ let logger: ReturnType;
+ let serializer: jest.Mocked;
+
+ const registry = createRegistry();
+ const documentMigrator = createDocumentMigrator(registry);
+
+ const expectSuccess = ({ type, id }: { type: string; id: string }) => {
+ // @ts-expect-error TS is not aware of the extension
+ return expect.toBeDocumentWithoutError(type, id);
+ };
+
+ const expectMigrationArgs = (args: unknown, contains = true, n = 1) => {
+ const obj = contains ? expect.objectContaining(args) : expect.not.objectContaining(args);
+ expect(migrator.migrateDocument).toHaveBeenNthCalledWith(
+ n,
+ obj,
+ expect.objectContaining({
+ allowDowngrade: expect.any(Boolean),
+ })
+ );
+ };
+
+ beforeEach(() => {
+ pointInTimeFinderMock.mockClear();
+ client = elasticsearchClientMock.createElasticsearchClient();
+ migrator = kibanaMigratorMock.create();
+ documentMigrator.prepareMigrations();
+ migrator.migrateDocument = jest.fn().mockImplementation(documentMigrator.migrate);
+ migrator.runMigrations = jest.fn().mockResolvedValue([{ status: 'skipped' }]);
+ logger = loggerMock.create();
+
+ // create a mock serializer "shim" so we can track function calls, but use the real serializer's implementation
+ serializer = createSpySerializer(registry);
+
+ const allTypes = registry.getAllTypes().map((type) => type.name);
+ const allowedTypes = [...new Set(allTypes.filter((type) => !registry.isHidden(type)))];
+
+ // @ts-expect-error must use the private constructor to use the mocked serializer
+ repository = new SavedObjectsRepository({
+ index: '.kibana-test',
+ mappings,
+ client,
+ migrator,
+ typeRegistry: registry,
+ serializer,
+ allowedTypes,
+ logger,
+ });
+
+ mockGetCurrentTime.mockReturnValue(mockTimestamp);
+ mockGetSearchDsl.mockClear();
+ });
+
+ // Setup migration mock for creating an object
+ const mockMigrationVersion = { foo: '2.3.4' };
+ const mockMigrateDocument = (doc: SavedObjectUnsanitizedDoc) => ({
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ ...(doc.attributes?.title && { title: `${doc.attributes.title}!!` }),
+ },
+ migrationVersion: mockMigrationVersion,
+ managed: doc.managed ?? false,
+ references: [{ name: 'search_0', type: 'search', id: '123' }],
+ });
+
+ describe('#bulkCreate', () => {
+ beforeEach(() => {
+ mockPreflightCheckForCreate.mockReset();
+ mockPreflightCheckForCreate.mockImplementation(({ objects }) => {
+ return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default
+ });
+ });
+
+ const obj1 = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ references: [{ name: 'ref_0', type: 'test', id: '1' }],
+ managed: false,
+ };
+ const obj2 = {
+ type: 'index-pattern',
+ id: 'logstash-*',
+ attributes: { title: 'Test Two' },
+ references: [{ name: 'ref_0', type: 'test', id: '2' }],
+ managed: false,
+ };
+ const namespace = 'foo-namespace';
+
+ // bulk create calls have two objects for each source -- the action, and the source
+ const expectClientCallArgsAction = (
+ objects: Array<{ type: string; id?: string; if_primary_term?: string; if_seq_no?: string }>,
+ {
+ method,
+ _index = expect.any(String),
+ getId = () => expect.any(String),
+ }: { method: string; _index?: string; getId?: (type: string, id?: string) => string }
+ ) => {
+ const body = [];
+ for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) {
+ body.push({
+ [method]: {
+ _index,
+ _id: getId(type, id),
+ ...(ifPrimaryTerm && ifSeqNo
+ ? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) }
+ : {}),
+ },
+ });
+ body.push(expect.any(Object));
+ }
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ };
+
+ const expectObjArgs = (
+ {
+ type,
+ attributes,
+ references,
+ }: { type: string; attributes: unknown; references?: SavedObjectReference[] },
+ overrides: Record = {}
+ ) => [
+ expect.any(Object),
+ expect.objectContaining({
+ [type]: attributes,
+ references,
+ type,
+ ...overrides,
+ ...mockTimestampFields,
+ }),
+ ];
+ describe('client calls', () => {
+ it(`should use the ES bulk action by default`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expect(client.bulk).toHaveBeenCalledTimes(1);
+ });
+
+ it(`should use the preflightCheckForCreate action before bulk action for any types that are multi-namespace, when id is defined`, async () => {
+ const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }];
+ await bulkCreateSuccess(client, repository, objects);
+ expect(client.bulk).toHaveBeenCalledTimes(1);
+ expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1);
+ expect(mockPreflightCheckForCreate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ objects: [
+ {
+ type: MULTI_NAMESPACE_ISOLATED_TYPE,
+ id: obj2.id,
+ overwrite: false,
+ namespaces: ['default'],
+ },
+ ],
+ })
+ );
+ });
+
+ it(`should use the ES create method if ID is undefined and overwrite=true`, async () => {
+ const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined }));
+ await bulkCreateSuccess(client, repository, objects, { overwrite: true });
+ expectClientCallArgsAction(objects, { method: 'create' });
+ });
+
+ it(`should use the ES create method if ID is undefined and overwrite=false`, async () => {
+ const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined }));
+ await bulkCreateSuccess(client, repository, objects);
+ expectClientCallArgsAction(objects, { method: 'create' });
+ });
+
+ it(`should use the ES index method if ID is defined and overwrite=true`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { overwrite: true });
+ expectClientCallArgsAction([obj1, obj2], { method: 'index' });
+ });
+
+ it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => {
+ await bulkCreateSuccess(
+ client,
+ repository,
+ [
+ {
+ ...obj1,
+ version: mockVersion,
+ },
+ obj2,
+ ],
+ { overwrite: true }
+ );
+
+ const obj1WithSeq = {
+ ...obj1,
+ managed: obj1.managed,
+ if_seq_no: mockVersionProps._seq_no,
+ if_primary_term: mockVersionProps._primary_term,
+ };
+
+ expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' });
+ });
+
+ it(`should use the ES create method if ID is defined and overwrite=false`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expectClientCallArgsAction([obj1, obj2], { method: 'create' });
+ });
+
+ it(`should use the ES index method if ID is defined, overwrite=true and managed=true in a document`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], {
+ overwrite: true,
+ managed: true,
+ });
+ expectClientCallArgsAction([obj1, obj2], { method: 'index' });
+ });
+
+ it(`should use the ES create method if ID is defined, overwrite=false and managed=true in a document`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { managed: true });
+ expectClientCallArgsAction([obj1, obj2], { method: 'create' });
+ });
+
+ it(`formats the ES request`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+ // this test only ensures that the client accepts the managed field in a document
+ it(`formats the ES request with managed=true in a document`, async () => {
+ const obj1WithManagedTrue = { ...obj1, managed: true };
+ const obj2WithManagedTrue = { ...obj2, managed: true };
+ await bulkCreateSuccess(client, repository, [obj1WithManagedTrue, obj2WithManagedTrue]);
+ const body = [...expectObjArgs(obj1WithManagedTrue), ...expectObjArgs(obj2WithManagedTrue)];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ describe('originId', () => {
+ it(`returns error if originId is set for non-multi-namespace type`, async () => {
+ const result = await repository.bulkCreate([
+ { ...obj1, originId: 'some-originId' },
+ { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE, originId: 'some-originId' },
+ ]);
+ expect(result.saved_objects).toEqual([
+ expect.objectContaining({ id: obj1.id, type: obj1.type, error: expect.anything() }),
+ expect.objectContaining({
+ id: obj2.id,
+ type: NAMESPACE_AGNOSTIC_TYPE,
+ error: expect.anything(),
+ }),
+ ]);
+ expect(client.bulk).not.toHaveBeenCalled();
+ });
+
+ it(`defaults to no originId`, async () => {
+ const objects = [
+ { ...obj1, type: MULTI_NAMESPACE_TYPE },
+ { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
+ ];
+
+ await bulkCreateSuccess(client, repository, objects);
+ const expected = expect.not.objectContaining({ originId: expect.anything() });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ describe('with existing originId', () => {
+ beforeEach(() => {
+ mockPreflightCheckForCreate.mockImplementation(({ objects }) => {
+ const existingDocument = {
+ _source: { originId: 'existing-originId' },
+ } as SavedObjectsRawDoc;
+ return Promise.resolve(
+ objects.map(({ type, id }) => ({ type, id, existingDocument }))
+ );
+ });
+ });
+
+ it(`accepts custom originId for multi-namespace type`, async () => {
+ // The preflight result has `existing-originId`, but that is discarded
+ const objects = [
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: 'some-originId' },
+ { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: 'some-originId' },
+ ];
+ await bulkCreateSuccess(client, repository, objects);
+ const expected = expect.objectContaining({ originId: 'some-originId' });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ it(`accepts undefined originId`, async () => {
+ // The preflight result has `existing-originId`, but that is discarded
+ const objects = [
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: undefined },
+ { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: undefined },
+ ];
+ await bulkCreateSuccess(client, repository, objects);
+ const expected = expect.not.objectContaining({ originId: expect.anything() });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ it(`preserves existing originId if originId option is not set`, async () => {
+ const objects = [
+ { ...obj1, type: MULTI_NAMESPACE_TYPE },
+ { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
+ ];
+ await bulkCreateSuccess(client, repository, objects);
+ const expected = expect.objectContaining({ originId: 'existing-originId' });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+ });
+ });
+
+ it(`adds namespace to request body for any types that are single-namespace`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
+ const expected = expect.objectContaining({ namespace });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ // this only ensures we don't override any other options
+ it(`adds managed=false to request body if declared for any types that are single-namespace`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: false });
+ const expected = expect.objectContaining({ namespace, managed: false });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+ // this only ensures we don't override any other options
+ it(`adds managed=true to request body if declared for any types that are single-namespace`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: true });
+ const expected = expect.objectContaining({ namespace, managed: true });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ it(`normalizes options.namespace from 'default' to undefined`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace: 'default' });
+ const expected = expect.not.objectContaining({ namespace: 'default' });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => {
+ const objects = [
+ { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
+ { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
+ ];
+ await bulkCreateSuccess(client, repository, objects, { namespace });
+ const expected = expect.not.objectContaining({ namespace: expect.anything() });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ });
+
+ it(`adds namespaces to request body for any types that are multi-namespace`, async () => {
+ const test = async (namespace?: string) => {
+ const objects = [obj1, obj2].map((x) => ({ ...x, type: MULTI_NAMESPACE_ISOLATED_TYPE }));
+ const [o1, o2] = objects;
+ mockPreflightCheckForCreate.mockResolvedValueOnce([
+ { type: o1.type, id: o1.id! }, // first object does not have an existing document to overwrite
+ {
+ type: o2.type,
+ id: o2.id!,
+ existingDocument: { _id: o2.id!, _source: { namespaces: ['*'], type: o2.type } }, // second object does have an existing document to overwrite
+ },
+ ]);
+ await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
+ const expected1 = expect.objectContaining({ namespaces: [namespace ?? 'default'] });
+ const expected2 = expect.objectContaining({ namespaces: ['*'] });
+ const body = [expect.any(Object), expected1, expect.any(Object), expected2];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ client.bulk.mockClear();
+ mockPreflightCheckForCreate.mockReset();
+ };
+ await test(undefined);
+ await test(namespace);
+ });
+
+ it(`adds initialNamespaces instead of namespace`, async () => {
+ const test = async (namespace?: string) => {
+ const ns2 = 'bar-namespace';
+ const ns3 = 'baz-namespace';
+ const objects = [
+ { ...obj1, type: 'dashboard', initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] },
+ ];
+ const [o1, o2, o3] = objects;
+ mockPreflightCheckForCreate.mockResolvedValueOnce([
+ // first object does not get passed in to preflightCheckForCreate at all
+ { type: o2.type, id: o2.id! }, // second object does not have an existing document to overwrite
+ {
+ type: o3.type,
+ id: o3.id!,
+ existingDocument: {
+ _id: o3.id!,
+ _source: { type: o3.type, namespaces: [namespace ?? 'default', 'something-else'] }, // third object does have an existing document to overwrite
+ },
+ },
+ ]);
+ await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
+ const body = [
+ { index: expect.objectContaining({ _id: `${ns2}:dashboard:${o1.id}` }) },
+ expect.objectContaining({ namespace: ns2 }),
+ {
+ index: expect.objectContaining({
+ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${o2.id}`,
+ }),
+ },
+ expect.objectContaining({ namespaces: [ns2] }),
+ { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${o3.id}` }) },
+ expect.objectContaining({ namespaces: [ns2, ns3] }),
+ ];
+ expect(mockPreflightCheckForCreate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ objects: [
+ // assert that the initialNamespaces fields were passed into preflightCheckForCreate instead of the current namespace
+ { type: o2.type, id: o2.id, overwrite: true, namespaces: o2.initialNamespaces },
+ { type: o3.type, id: o3.id, overwrite: true, namespaces: o3.initialNamespaces },
+ ],
+ })
+ );
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ client.bulk.mockClear();
+ mockPreflightCheckForCreate.mockReset();
+ };
+ await test(undefined);
+ await test(namespace);
+ });
+
+ it(`normalizes initialNamespaces from 'default' to undefined`, async () => {
+ const test = async (namespace?: string) => {
+ const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }];
+ await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
+ const body = [
+ { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) },
+ expect.not.objectContaining({ namespace: 'default' }),
+ ];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ client.bulk.mockClear();
+ };
+ await test(undefined);
+ await test(namespace);
+ });
+
+ it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => {
+ const test = async (namespace?: string) => {
+ const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }];
+ await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
+ const expected = expect.not.objectContaining({ namespaces: expect.anything() });
+ const body = [expect.any(Object), expected, expect.any(Object), expected];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ client.bulk.mockClear();
+ };
+ await test(undefined);
+ await test(namespace);
+ });
+
+ it(`defaults to a refresh setting of wait_for`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ refresh: 'wait_for' }),
+ expect.anything()
+ );
+ });
+
+ it(`should use default index`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expectClientCallArgsAction([obj1, obj2], {
+ method: 'create',
+ _index: '.kibana-test_8.0.0-testing',
+ });
+ });
+
+ it(`should use custom index`, async () => {
+ await bulkCreateSuccess(
+ client,
+ repository,
+ [obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE }))
+ );
+ expectClientCallArgsAction([obj1, obj2], {
+ method: 'create',
+ _index: 'custom_8.0.0-testing',
+ });
+ });
+
+ it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => {
+ const getId = (type: string, id: string = '') => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix)
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
+ expectClientCallArgsAction([obj1, obj2], { method: 'create', getId });
+ });
+
+ it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => {
+ const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix)
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expectClientCallArgsAction([obj1, obj2], { method: 'create', getId });
+ });
+
+ it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => {
+ const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix)
+ const objects = [
+ { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
+ { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
+ ];
+ await bulkCreateSuccess(client, repository, objects, { namespace });
+ expectClientCallArgsAction(objects, { method: 'create', getId });
+ });
+ });
+
+ describe('errors', () => {
+ afterEach(() => {
+ mockGetBulkOperationError.mockReset();
+ });
+
+ const obj3 = {
+ type: 'dashboard',
+ id: 'three',
+ attributes: { title: 'Test Three' },
+ references: [{ name: 'ref_0', type: 'test', id: '2' }],
+ };
+
+ const bulkCreateError = async (
+ obj: SavedObjectsBulkCreateObject,
+ isBulkError: boolean | undefined,
+ expectedErrorResult: ExpectedErrorResult
+ ) => {
+ let response;
+ if (isBulkError) {
+ // mock the bulk error for only the second object
+ mockGetBulkOperationError.mockReturnValueOnce(undefined);
+ mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload);
+ response = getMockBulkCreateResponse([obj1, obj, obj2]);
+ } else {
+ response = getMockBulkCreateResponse([obj1, obj2]);
+ }
+ client.bulk.mockResponseOnce(response);
+
+ const objects = [obj1, obj, obj2];
+ const result = await repository.bulkCreate(objects);
+ expect(client.bulk).toHaveBeenCalled();
+ const objCall = isBulkError ? expectObjArgs(obj) : [];
+ const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)];
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body }),
+ expect.anything()
+ );
+ expect(result).toEqual({
+ saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)],
+ });
+ };
+
+ it(`throws when options.namespace is '*'`, async () => {
+ await expect(
+ repository.bulkCreate([obj3], { namespace: ALL_NAMESPACES_STRING })
+ ).rejects.toThrowError(createBadRequestErrorPayload('"options.namespace" cannot be "*"'));
+ });
+
+ it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => {
+ const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestErrorPayload(
+ '"initialNamespaces" cannot be used on space-agnostic types'
+ )
+ )
+ );
+ });
+
+ it(`returns error when initialNamespaces is empty`, async () => {
+ const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestErrorPayload('"initialNamespaces" must be a non-empty array of strings')
+ )
+ );
+ });
+
+ it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType: string, initialNamespaces: string[]) => {
+ const obj = { ...obj3, type: objType, initialNamespaces };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestErrorPayload(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
+ it(`returns error when type is invalid`, async () => {
+ const obj = { ...obj3, type: 'unknownType' };
+ await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
+ });
+
+ it(`returns error when type is hidden`, async () => {
+ const obj = { ...obj3, type: HIDDEN_TYPE };
+ await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
+ });
+
+ it(`returns error when there is a conflict from preflightCheckForCreate`, async () => {
+ const objects = [
+ // only the second, third, and fourth objects are passed to preflightCheckForCreate and result in errors
+ obj1,
+ { ...obj1, type: MULTI_NAMESPACE_TYPE },
+ { ...obj2, type: MULTI_NAMESPACE_TYPE },
+ { ...obj3, type: MULTI_NAMESPACE_TYPE },
+ obj2,
+ ];
+ const [o1, o2, o3, o4, o5] = objects;
+ mockPreflightCheckForCreate.mockResolvedValueOnce([
+ // first and last objects do not get passed in to preflightCheckForCreate at all
+ { type: o2.type, id: o2.id!, error: { type: 'conflict' } },
+ {
+ type: o3.type,
+ id: o3.id!,
+ error: { type: 'unresolvableConflict', metadata: { isNotOverwritable: true } },
+ },
+ {
+ type: o4.type,
+ id: o4.id!,
+ error: { type: 'aliasConflict', metadata: { spacesWithConflictingAliases: ['foo'] } },
+ },
+ ]);
+ const bulkResponse = getMockBulkCreateResponse([o1, o5]);
+ client.bulk.mockResponseOnce(bulkResponse);
+
+ const options = { overwrite: true };
+ const result = await repository.bulkCreate(objects, options);
+ expect(mockPreflightCheckForCreate).toHaveBeenCalled();
+ expect(mockPreflightCheckForCreate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ objects: [
+ { type: o2.type, id: o2.id, overwrite: true, namespaces: ['default'] },
+ { type: o3.type, id: o3.id, overwrite: true, namespaces: ['default'] },
+ { type: o4.type, id: o4.id, overwrite: true, namespaces: ['default'] },
+ ],
+ })
+ );
+ expect(client.bulk).toHaveBeenCalled();
+ expect(client.bulk).toHaveBeenCalledWith(
+ expect.objectContaining({ body: [...expectObjArgs(o1), ...expectObjArgs(o5)] }),
+ expect.anything()
+ );
+ expect(result).toEqual({
+ saved_objects: [
+ expectSuccess(o1),
+ expectErrorConflict(o2),
+ expectErrorConflict(o3, { metadata: { isNotOverwritable: true } }),
+ expectErrorConflict(o4, { metadata: { spacesWithConflictingAliases: ['foo'] } }),
+ expectSuccess(o5),
+ ],
+ });
+ });
+
+ it(`returns bulk error`, async () => {
+ const expectedErrorResult = {
+ type: obj3.type,
+ id: obj3.id,
+ error: { error: 'Oh no, a bulk error!' },
+ };
+ await bulkCreateError(obj3, true, expectedErrorResult);
+ });
+
+ it(`returns errors for any bulk objects with invalid schemas`, async () => {
+ const response = getMockBulkCreateResponse([obj3]);
+ client.bulk.mockResponseOnce(response);
+
+ const result = await repository.bulkCreate([
+ obj3,
+ // @ts-expect-error - Title should be a string and is intentionally malformed for testing
+ { ...obj3, id: 'three-again', attributes: { title: 123 } },
+ ]);
+ expect(client.bulk).toHaveBeenCalledTimes(1); // only called once for the valid object
+ expect(result.saved_objects).toEqual([
+ expect.objectContaining(obj3),
+ expect.objectContaining({
+ error: new Error(
+ '[attributes.title]: expected value of type [string] but got [number]: Bad Request'
+ ),
+ id: 'three-again',
+ type: 'dashboard',
+ }),
+ ]);
+ });
+ });
+
+ describe('migration', () => {
+ it(`migrates the docs and serializes the migrated docs`, async () => {
+ migrator.migrateDocument.mockImplementation(mockMigrateDocument);
+ const modifiedObj1 = { ...obj1, coreMigrationVersion: '8.0.0' };
+ await bulkCreateSuccess(client, repository, [modifiedObj1, obj2]);
+ const docs = [modifiedObj1, obj2].map((x) => ({ ...x, ...mockTimestampFieldsWithCreated }));
+ expectMigrationArgs(docs[0], true, 1);
+ expectMigrationArgs(docs[1], true, 2);
+
+ const migratedDocs = docs.map((x) => migrator.migrateDocument(x));
+ expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]);
+ expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]);
+ });
+
+ it(`adds namespace to body when providing namespace for single-namespace type`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
+ expectMigrationArgs({ namespace }, true, 1);
+ expectMigrationArgs({ namespace }, true, 2);
+ });
+
+ it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => {
+ await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expectMigrationArgs({ namespace: expect.anything() }, false, 1);
+ expectMigrationArgs({ namespace: expect.anything() }, false, 2);
+ });
+
+ it(`doesn't add namespace to body when not using single-namespace type`, async () => {
+ const objects = [
+ { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
+ { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
+ ];
+ await bulkCreateSuccess(client, repository, objects, { namespace });
+ expectMigrationArgs({ namespace: expect.anything() }, false, 1);
+ expectMigrationArgs({ namespace: expect.anything() }, false, 2);
+ });
+
+ it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => {
+ const objects = [obj1, obj2].map((obj) => ({
+ ...obj,
+ type: MULTI_NAMESPACE_ISOLATED_TYPE,
+ }));
+ await bulkCreateSuccess(client, repository, objects, { namespace });
+ expectMigrationArgs({ namespaces: [namespace] }, true, 1);
+ expectMigrationArgs({ namespaces: [namespace] }, true, 2);
+ });
+
+ it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => {
+ const objects = [obj1, obj2].map((obj) => ({
+ ...obj,
+ type: MULTI_NAMESPACE_ISOLATED_TYPE,
+ }));
+ await bulkCreateSuccess(client, repository, objects);
+ expectMigrationArgs({ namespaces: ['default'] }, true, 1);
+ expectMigrationArgs({ namespaces: ['default'] }, true, 2);
+ });
+
+ it(`doesn't add namespaces to body when not using multi-namespace type`, async () => {
+ const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }];
+ await bulkCreateSuccess(client, repository, objects);
+ expectMigrationArgs({ namespaces: expect.anything() }, false, 1);
+ expectMigrationArgs({ namespaces: expect.anything() }, false, 2);
+ });
+ });
+
+ describe('returns', () => {
+ it(`formats the ES response`, async () => {
+ const result = await bulkCreateSuccess(client, repository, [obj1, obj2]);
+ expect(result).toEqual({
+ saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
+ });
+ });
+
+ it.todo(`should return objects in the same order regardless of type`);
+
+ it(`handles a mix of successful creates and errors`, async () => {
+ const obj = {
+ type: 'unknownType',
+ id: 'three',
+ attributes: {},
+ };
+ const objects = [obj1, obj, obj2];
+ const response = getMockBulkCreateResponse([obj1, obj2]);
+ client.bulk.mockResponseOnce(response);
+ const result = await repository.bulkCreate(objects);
+ expect(client.bulk).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({
+ saved_objects: [expectCreateResult(obj1), expectError(obj), expectCreateResult(obj2)],
+ });
+ });
+
+ it(`a deserialized saved object`, async () => {
+ // Test for fix to https://github.com/elastic/kibana/issues/65088 where
+ // we returned raw ID's when an object without an id was created.
+ const namespace = 'myspace';
+ // FIXME: this test is based on a gigantic hack to have the bulk operation return the source
+ // of the document when it actually does not, forcing to cast to any as BulkResponse
+ // does not contains _source
+ const response = getMockBulkCreateResponse([obj1, obj2], namespace) as any;
+ client.bulk.mockResponseOnce(response);
+
+ // Bulk create one object with id unspecified, and one with id specified
+ const result = await repository.bulkCreate([{ ...obj1, id: undefined }, obj2], {
+ namespace,
+ });
+
+ // Assert that both raw docs from the ES response are deserialized
+ expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(
+ 1,
+ {
+ ...response.items[0].create,
+ _source: {
+ ...response.items[0].create._source,
+ namespaces: response.items[0].create._source.namespaces,
+ coreMigrationVersion: expect.any(String),
+ typeMigrationVersion: '1.1.1',
+ },
+ _id: expect.stringMatching(
+ /^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/
+ ),
+ },
+ expect.any(Object)
+ );
+ expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(
+ 2,
+ {
+ ...response.items[1].create,
+ _source: {
+ ...response.items[1].create._source,
+ namespaces: response.items[1].create._source.namespaces,
+ coreMigrationVersion: expect.any(String),
+ typeMigrationVersion: '1.1.1',
+ },
+ },
+ expect.any(Object)
+ );
+
+ // Assert that ID's are deserialized to remove the type and namespace
+ expect(result.saved_objects[0].id).toEqual(
+ expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/)
+ );
+ expect(result.saved_objects[1].id).toEqual(obj2.id);
+
+ // Assert that managed is not changed
+ expect(result.saved_objects[0].managed).toBeFalsy();
+ expect(result.saved_objects[1].managed).toEqual(obj2.managed);
+ });
+
+ it(`sets managed=false if not already set`, async () => {
+ const obj1WithoutManaged = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ references: [{ name: 'ref_0', type: 'test', id: '1' }],
+ };
+ const obj2WithoutManaged = {
+ type: 'index-pattern',
+ id: 'logstash-*',
+ attributes: { title: 'Test Two' },
+ references: [{ name: 'ref_0', type: 'test', id: '2' }],
+ };
+ const result = await bulkCreateSuccess(client, repository, [
+ obj1WithoutManaged,
+ obj2WithoutManaged,
+ ]);
+ expect(result).toEqual({
+ saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
+ });
+ });
+
+ it(`sets managed=false only on documents without managed already set`, async () => {
+ const objWithoutManaged = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ references: [{ name: 'ref_0', type: 'test', id: '1' }],
+ };
+ const result = await bulkCreateSuccess(client, repository, [objWithoutManaged, obj2]);
+ expect(result).toEqual({
+ saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
+ });
+ });
+
+ it(`sets managed=true if provided as an override`, async () => {
+ const obj1WithoutManaged = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ references: [{ name: 'ref_0', type: 'test', id: '1' }],
+ };
+ const obj2WithoutManaged = {
+ type: 'index-pattern',
+ id: 'logstash-*',
+ attributes: { title: 'Test Two' },
+ references: [{ name: 'ref_0', type: 'test', id: '2' }],
+ };
+ const result = await bulkCreateSuccess(
+ client,
+ repository,
+ [obj1WithoutManaged, obj2WithoutManaged],
+ { managed: true }
+ );
+ expect(result).toEqual({
+ saved_objects: [
+ { ...obj1WithoutManaged, managed: true },
+ { ...obj2WithoutManaged, managed: true },
+ ].map((x) => expectCreateResult(x)),
+ });
+ });
+
+ it(`sets managed=false if provided as an override`, async () => {
+ const obj1WithoutManaged = {
+ type: 'config',
+ id: '6.0.0-alpha1',
+ attributes: { title: 'Test One' },
+ references: [{ name: 'ref_0', type: 'test', id: '1' }],
+ };
+ const obj2WithoutManaged = {
+ type: 'index-pattern',
+ id: 'logstash-*',
+ attributes: { title: 'Test Two' },
+ references: [{ name: 'ref_0', type: 'test', id: '2' }],
+ };
+ const result = await bulkCreateSuccess(
+ client,
+ repository,
+ [obj1WithoutManaged, obj2WithoutManaged],
+ { managed: false }
+ );
+ expect(result).toEqual({
+ saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
+ });
+ });
+ });
+ });
+});
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
index 1084ad3e589661..3547d653e3de40 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
@@ -32,7 +32,6 @@ import type {
SavedObjectsIncrementCounterOptions,
SavedObjectsCreatePointInTimeFinderDependencies,
SavedObjectsCreatePointInTimeFinderOptions,
- SavedObjectsBulkCreateObject,
SavedObjectsBulkGetObject,
SavedObjectsCreateOptions,
SavedObjectsDeleteOptions,
@@ -86,13 +85,10 @@ import {
getMockMgetResponse,
type TypeIdTuple,
createSpySerializer,
- bulkCreateSuccess,
- getMockBulkCreateResponse,
bulkGet,
expectErrorResult,
expectErrorInvalidType,
expectErrorNotFound,
- expectErrorConflict,
expectError,
generateIndexPatternSearchResults,
findSuccess,
@@ -105,7 +101,6 @@ import {
createUnsupportedTypeErrorPayload,
createConflictErrorPayload,
createGenericNotFoundErrorPayload,
- expectCreateResult,
mockTimestampFieldsWithCreated,
getMockEsBulkDeleteResponse,
bulkDeleteSuccess,
@@ -193,917 +188,6 @@ describe('SavedObjectsRepository', () => {
references: [{ name: 'search_0', type: 'search', id: '123' }],
});
- describe('#bulkCreate', () => {
- beforeEach(() => {
- mockPreflightCheckForCreate.mockReset();
- mockPreflightCheckForCreate.mockImplementation(({ objects }) => {
- return Promise.resolve(objects.map(({ type, id }) => ({ type, id }))); // respond with no errors by default
- });
- });
-
- const obj1 = {
- type: 'config',
- id: '6.0.0-alpha1',
- attributes: { title: 'Test One' },
- references: [{ name: 'ref_0', type: 'test', id: '1' }],
- managed: false,
- };
- const obj2 = {
- type: 'index-pattern',
- id: 'logstash-*',
- attributes: { title: 'Test Two' },
- references: [{ name: 'ref_0', type: 'test', id: '2' }],
- managed: false,
- };
- const namespace = 'foo-namespace';
-
- // bulk create calls have two objects for each source -- the action, and the source
- const expectClientCallArgsAction = (
- objects: Array<{ type: string; id?: string; if_primary_term?: string; if_seq_no?: string }>,
- {
- method,
- _index = expect.any(String),
- getId = () => expect.any(String),
- }: { method: string; _index?: string; getId?: (type: string, id?: string) => string }
- ) => {
- const body = [];
- for (const { type, id, if_primary_term: ifPrimaryTerm, if_seq_no: ifSeqNo } of objects) {
- body.push({
- [method]: {
- _index,
- _id: getId(type, id),
- ...(ifPrimaryTerm && ifSeqNo
- ? { if_primary_term: expect.any(Number), if_seq_no: expect.any(Number) }
- : {}),
- },
- });
- body.push(expect.any(Object));
- }
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- };
-
- const expectObjArgs = (
- {
- type,
- attributes,
- references,
- }: { type: string; attributes: unknown; references?: SavedObjectReference[] },
- overrides: Record = {}
- ) => [
- expect.any(Object),
- expect.objectContaining({
- [type]: attributes,
- references,
- type,
- ...overrides,
- ...mockTimestampFields,
- }),
- ];
- describe('client calls', () => {
- it(`should use the ES bulk action by default`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2]);
- expect(client.bulk).toHaveBeenCalledTimes(1);
- });
-
- it(`should use the preflightCheckForCreate action before bulk action for any types that are multi-namespace, when id is defined`, async () => {
- const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }];
- await bulkCreateSuccess(client, repository, objects);
- expect(client.bulk).toHaveBeenCalledTimes(1);
- expect(mockPreflightCheckForCreate).toHaveBeenCalledTimes(1);
- expect(mockPreflightCheckForCreate).toHaveBeenCalledWith(
- expect.objectContaining({
- objects: [
- {
- type: MULTI_NAMESPACE_ISOLATED_TYPE,
- id: obj2.id,
- overwrite: false,
- namespaces: ['default'],
- },
- ],
- })
- );
- });
-
- it(`should use the ES create method if ID is undefined and overwrite=true`, async () => {
- const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined }));
- await bulkCreateSuccess(client, repository, objects, { overwrite: true });
- expectClientCallArgsAction(objects, { method: 'create' });
- });
-
- it(`should use the ES create method if ID is undefined and overwrite=false`, async () => {
- const objects = [obj1, obj2].map((obj) => ({ ...obj, id: undefined }));
- await bulkCreateSuccess(client, repository, objects);
- expectClientCallArgsAction(objects, { method: 'create' });
- });
-
- it(`should use the ES index method if ID is defined and overwrite=true`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], { overwrite: true });
- expectClientCallArgsAction([obj1, obj2], { method: 'index' });
- });
-
- it(`should use the ES index method with version if ID and version are defined and overwrite=true`, async () => {
- await bulkCreateSuccess(
- client,
- repository,
- [
- {
- ...obj1,
- version: mockVersion,
- },
- obj2,
- ],
- { overwrite: true }
- );
-
- const obj1WithSeq = {
- ...obj1,
- managed: obj1.managed,
- if_seq_no: mockVersionProps._seq_no,
- if_primary_term: mockVersionProps._primary_term,
- };
-
- expectClientCallArgsAction([obj1WithSeq, obj2], { method: 'index' });
- });
-
- it(`should use the ES create method if ID is defined and overwrite=false`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2]);
- expectClientCallArgsAction([obj1, obj2], { method: 'create' });
- });
-
- it(`should use the ES index method if ID is defined, overwrite=true and managed=true in a document`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], {
- overwrite: true,
- managed: true,
- });
- expectClientCallArgsAction([obj1, obj2], { method: 'index' });
- });
-
- it(`should use the ES create method if ID is defined, overwrite=false and managed=true in a document`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], { managed: true });
- expectClientCallArgsAction([obj1, obj2], { method: 'create' });
- });
-
- it(`formats the ES request`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2]);
- const body = [...expectObjArgs(obj1), ...expectObjArgs(obj2)];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
- // this test only ensures that the client accepts the managed field in a document
- it(`formats the ES request with managed=true in a document`, async () => {
- const obj1WithManagedTrue = { ...obj1, managed: true };
- const obj2WithManagedTrue = { ...obj2, managed: true };
- await bulkCreateSuccess(client, repository, [obj1WithManagedTrue, obj2WithManagedTrue]);
- const body = [...expectObjArgs(obj1WithManagedTrue), ...expectObjArgs(obj2WithManagedTrue)];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- describe('originId', () => {
- it(`returns error if originId is set for non-multi-namespace type`, async () => {
- const result = await repository.bulkCreate([
- { ...obj1, originId: 'some-originId' },
- { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE, originId: 'some-originId' },
- ]);
- expect(result.saved_objects).toEqual([
- expect.objectContaining({ id: obj1.id, type: obj1.type, error: expect.anything() }),
- expect.objectContaining({
- id: obj2.id,
- type: NAMESPACE_AGNOSTIC_TYPE,
- error: expect.anything(),
- }),
- ]);
- expect(client.bulk).not.toHaveBeenCalled();
- });
-
- it(`defaults to no originId`, async () => {
- const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE },
- { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
- ];
-
- await bulkCreateSuccess(client, repository, objects);
- const expected = expect.not.objectContaining({ originId: expect.anything() });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- describe('with existing originId', () => {
- beforeEach(() => {
- mockPreflightCheckForCreate.mockImplementation(({ objects }) => {
- const existingDocument = {
- _source: { originId: 'existing-originId' },
- } as SavedObjectsRawDoc;
- return Promise.resolve(
- objects.map(({ type, id }) => ({ type, id, existingDocument }))
- );
- });
- });
-
- it(`accepts custom originId for multi-namespace type`, async () => {
- // The preflight result has `existing-originId`, but that is discarded
- const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: 'some-originId' },
- { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: 'some-originId' },
- ];
- await bulkCreateSuccess(client, repository, objects);
- const expected = expect.objectContaining({ originId: 'some-originId' });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- it(`accepts undefined originId`, async () => {
- // The preflight result has `existing-originId`, but that is discarded
- const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE, originId: undefined },
- { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE, originId: undefined },
- ];
- await bulkCreateSuccess(client, repository, objects);
- const expected = expect.not.objectContaining({ originId: expect.anything() });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- it(`preserves existing originId if originId option is not set`, async () => {
- const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE },
- { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
- ];
- await bulkCreateSuccess(client, repository, objects);
- const expected = expect.objectContaining({ originId: 'existing-originId' });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
- });
- });
-
- it(`adds namespace to request body for any types that are single-namespace`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
- const expected = expect.objectContaining({ namespace });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- // this only ensures we don't override any other options
- it(`adds managed=false to request body if declared for any types that are single-namespace`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: false });
- const expected = expect.objectContaining({ namespace, managed: false });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
- // this only ensures we don't override any other options
- it(`adds managed=true to request body if declared for any types that are single-namespace`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, managed: true });
- const expected = expect.objectContaining({ namespace, managed: true });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- it(`normalizes options.namespace from 'default' to undefined`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace: 'default' });
- const expected = expect.not.objectContaining({ namespace: 'default' });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => {
- const objects = [
- { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
- { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
- ];
- await bulkCreateSuccess(client, repository, objects, { namespace });
- const expected = expect.not.objectContaining({ namespace: expect.anything() });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- });
-
- it(`adds namespaces to request body for any types that are multi-namespace`, async () => {
- const test = async (namespace?: string) => {
- const objects = [obj1, obj2].map((x) => ({ ...x, type: MULTI_NAMESPACE_ISOLATED_TYPE }));
- const [o1, o2] = objects;
- mockPreflightCheckForCreate.mockResolvedValueOnce([
- { type: o1.type, id: o1.id! }, // first object does not have an existing document to overwrite
- {
- type: o2.type,
- id: o2.id!,
- existingDocument: { _id: o2.id!, _source: { namespaces: ['*'], type: o2.type } }, // second object does have an existing document to overwrite
- },
- ]);
- await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
- const expected1 = expect.objectContaining({ namespaces: [namespace ?? 'default'] });
- const expected2 = expect.objectContaining({ namespaces: ['*'] });
- const body = [expect.any(Object), expected1, expect.any(Object), expected2];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- client.bulk.mockClear();
- mockPreflightCheckForCreate.mockReset();
- };
- await test(undefined);
- await test(namespace);
- });
-
- it(`adds initialNamespaces instead of namespace`, async () => {
- const test = async (namespace?: string) => {
- const ns2 = 'bar-namespace';
- const ns3 = 'baz-namespace';
- const objects = [
- { ...obj1, type: 'dashboard', initialNamespaces: [ns2] },
- { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] },
- { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] },
- ];
- const [o1, o2, o3] = objects;
- mockPreflightCheckForCreate.mockResolvedValueOnce([
- // first object does not get passed in to preflightCheckForCreate at all
- { type: o2.type, id: o2.id! }, // second object does not have an existing document to overwrite
- {
- type: o3.type,
- id: o3.id!,
- existingDocument: {
- _id: o3.id!,
- _source: { type: o3.type, namespaces: [namespace ?? 'default', 'something-else'] }, // third object does have an existing document to overwrite
- },
- },
- ]);
- await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
- const body = [
- { index: expect.objectContaining({ _id: `${ns2}:dashboard:${o1.id}` }) },
- expect.objectContaining({ namespace: ns2 }),
- {
- index: expect.objectContaining({
- _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${o2.id}`,
- }),
- },
- expect.objectContaining({ namespaces: [ns2] }),
- { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${o3.id}` }) },
- expect.objectContaining({ namespaces: [ns2, ns3] }),
- ];
- expect(mockPreflightCheckForCreate).toHaveBeenCalledWith(
- expect.objectContaining({
- objects: [
- // assert that the initialNamespaces fields were passed into preflightCheckForCreate instead of the current namespace
- { type: o2.type, id: o2.id, overwrite: true, namespaces: o2.initialNamespaces },
- { type: o3.type, id: o3.id, overwrite: true, namespaces: o3.initialNamespaces },
- ],
- })
- );
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- client.bulk.mockClear();
- mockPreflightCheckForCreate.mockReset();
- };
- await test(undefined);
- await test(namespace);
- });
-
- it(`normalizes initialNamespaces from 'default' to undefined`, async () => {
- const test = async (namespace?: string) => {
- const objects = [{ ...obj1, type: 'dashboard', initialNamespaces: ['default'] }];
- await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
- const body = [
- { index: expect.objectContaining({ _id: `dashboard:${obj1.id}` }) },
- expect.not.objectContaining({ namespace: 'default' }),
- ];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- client.bulk.mockClear();
- };
- await test(undefined);
- await test(namespace);
- });
-
- it(`doesn't add namespaces to request body for any types that are not multi-namespace`, async () => {
- const test = async (namespace?: string) => {
- const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }];
- await bulkCreateSuccess(client, repository, objects, { namespace, overwrite: true });
- const expected = expect.not.objectContaining({ namespaces: expect.anything() });
- const body = [expect.any(Object), expected, expect.any(Object), expected];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- client.bulk.mockClear();
- };
- await test(undefined);
- await test(namespace);
- });
-
- it(`defaults to a refresh setting of wait_for`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2]);
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ refresh: 'wait_for' }),
- expect.anything()
- );
- });
-
- it(`should use default index`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2]);
- expectClientCallArgsAction([obj1, obj2], {
- method: 'create',
- _index: '.kibana-test_8.0.0-testing',
- });
- });
-
- it(`should use custom index`, async () => {
- await bulkCreateSuccess(
- client,
- repository,
- [obj1, obj2].map((x) => ({ ...x, type: CUSTOM_INDEX_TYPE }))
- );
- expectClientCallArgsAction([obj1, obj2], {
- method: 'create',
- _index: 'custom_8.0.0-testing',
- });
- });
-
- it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => {
- const getId = (type: string, id: string = '') => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix)
- await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
- expectClientCallArgsAction([obj1, obj2], { method: 'create', getId });
- });
-
- it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => {
- const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix)
- await bulkCreateSuccess(client, repository, [obj1, obj2]);
- expectClientCallArgsAction([obj1, obj2], { method: 'create', getId });
- });
-
- it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => {
- const getId = (type: string, id: string = '') => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix)
- const objects = [
- { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
- { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
- ];
- await bulkCreateSuccess(client, repository, objects, { namespace });
- expectClientCallArgsAction(objects, { method: 'create', getId });
- });
- });
-
- describe('errors', () => {
- afterEach(() => {
- mockGetBulkOperationError.mockReset();
- });
-
- const obj3 = {
- type: 'dashboard',
- id: 'three',
- attributes: { title: 'Test Three' },
- references: [{ name: 'ref_0', type: 'test', id: '2' }],
- };
-
- const bulkCreateError = async (
- obj: SavedObjectsBulkCreateObject,
- isBulkError: boolean | undefined,
- expectedErrorResult: ExpectedErrorResult
- ) => {
- let response;
- if (isBulkError) {
- // mock the bulk error for only the second object
- mockGetBulkOperationError.mockReturnValueOnce(undefined);
- mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error as Payload);
- response = getMockBulkCreateResponse([obj1, obj, obj2]);
- } else {
- response = getMockBulkCreateResponse([obj1, obj2]);
- }
- client.bulk.mockResponseOnce(response);
-
- const objects = [obj1, obj, obj2];
- const result = await repository.bulkCreate(objects);
- expect(client.bulk).toHaveBeenCalled();
- const objCall = isBulkError ? expectObjArgs(obj) : [];
- const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)];
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body }),
- expect.anything()
- );
- expect(result).toEqual({
- saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)],
- });
- };
-
- it(`throws when options.namespace is '*'`, async () => {
- await expect(
- repository.bulkCreate([obj3], { namespace: ALL_NAMESPACES_STRING })
- ).rejects.toThrowError(createBadRequestErrorPayload('"options.namespace" cannot be "*"'));
- });
-
- it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => {
- const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] };
- await bulkCreateError(
- obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestErrorPayload(
- '"initialNamespaces" cannot be used on space-agnostic types'
- )
- )
- );
- });
-
- it(`returns error when initialNamespaces is empty`, async () => {
- const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] };
- await bulkCreateError(
- obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestErrorPayload('"initialNamespaces" must be a non-empty array of strings')
- )
- );
- });
-
- it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
- const doTest = async (objType: string, initialNamespaces: string[]) => {
- const obj = { ...obj3, type: objType, initialNamespaces };
- await bulkCreateError(
- obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestErrorPayload(
- '"initialNamespaces" can only specify a single space when used with space-isolated types'
- )
- )
- );
- };
- await doTest('dashboard', ['spacex', 'spacey']);
- await doTest('dashboard', ['*']);
- await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
- await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
- });
-
- it(`returns error when type is invalid`, async () => {
- const obj = { ...obj3, type: 'unknownType' };
- await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
- });
-
- it(`returns error when type is hidden`, async () => {
- const obj = { ...obj3, type: HIDDEN_TYPE };
- await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
- });
-
- it(`returns error when there is a conflict from preflightCheckForCreate`, async () => {
- const objects = [
- // only the second, third, and fourth objects are passed to preflightCheckForCreate and result in errors
- obj1,
- { ...obj1, type: MULTI_NAMESPACE_TYPE },
- { ...obj2, type: MULTI_NAMESPACE_TYPE },
- { ...obj3, type: MULTI_NAMESPACE_TYPE },
- obj2,
- ];
- const [o1, o2, o3, o4, o5] = objects;
- mockPreflightCheckForCreate.mockResolvedValueOnce([
- // first and last objects do not get passed in to preflightCheckForCreate at all
- { type: o2.type, id: o2.id!, error: { type: 'conflict' } },
- {
- type: o3.type,
- id: o3.id!,
- error: { type: 'unresolvableConflict', metadata: { isNotOverwritable: true } },
- },
- {
- type: o4.type,
- id: o4.id!,
- error: { type: 'aliasConflict', metadata: { spacesWithConflictingAliases: ['foo'] } },
- },
- ]);
- const bulkResponse = getMockBulkCreateResponse([o1, o5]);
- client.bulk.mockResponseOnce(bulkResponse);
-
- const options = { overwrite: true };
- const result = await repository.bulkCreate(objects, options);
- expect(mockPreflightCheckForCreate).toHaveBeenCalled();
- expect(mockPreflightCheckForCreate).toHaveBeenCalledWith(
- expect.objectContaining({
- objects: [
- { type: o2.type, id: o2.id, overwrite: true, namespaces: ['default'] },
- { type: o3.type, id: o3.id, overwrite: true, namespaces: ['default'] },
- { type: o4.type, id: o4.id, overwrite: true, namespaces: ['default'] },
- ],
- })
- );
- expect(client.bulk).toHaveBeenCalled();
- expect(client.bulk).toHaveBeenCalledWith(
- expect.objectContaining({ body: [...expectObjArgs(o1), ...expectObjArgs(o5)] }),
- expect.anything()
- );
- expect(result).toEqual({
- saved_objects: [
- expectSuccess(o1),
- expectErrorConflict(o2),
- expectErrorConflict(o3, { metadata: { isNotOverwritable: true } }),
- expectErrorConflict(o4, { metadata: { spacesWithConflictingAliases: ['foo'] } }),
- expectSuccess(o5),
- ],
- });
- });
-
- it(`returns bulk error`, async () => {
- const expectedErrorResult = {
- type: obj3.type,
- id: obj3.id,
- error: { error: 'Oh no, a bulk error!' },
- };
- await bulkCreateError(obj3, true, expectedErrorResult);
- });
-
- it(`returns errors for any bulk objects with invalid schemas`, async () => {
- const response = getMockBulkCreateResponse([obj3]);
- client.bulk.mockResponseOnce(response);
-
- const result = await repository.bulkCreate([
- obj3,
- // @ts-expect-error - Title should be a string and is intentionally malformed for testing
- { ...obj3, id: 'three-again', attributes: { title: 123 } },
- ]);
- expect(client.bulk).toHaveBeenCalledTimes(1); // only called once for the valid object
- expect(result.saved_objects).toEqual([
- expect.objectContaining(obj3),
- expect.objectContaining({
- error: new Error(
- '[attributes.title]: expected value of type [string] but got [number]: Bad Request'
- ),
- id: 'three-again',
- type: 'dashboard',
- }),
- ]);
- });
- });
-
- describe('migration', () => {
- it(`migrates the docs and serializes the migrated docs`, async () => {
- migrator.migrateDocument.mockImplementation(mockMigrateDocument);
- const modifiedObj1 = { ...obj1, coreMigrationVersion: '8.0.0' };
- await bulkCreateSuccess(client, repository, [modifiedObj1, obj2]);
- const docs = [modifiedObj1, obj2].map((x) => ({ ...x, ...mockTimestampFieldsWithCreated }));
- expectMigrationArgs(docs[0], true, 1);
- expectMigrationArgs(docs[1], true, 2);
-
- const migratedDocs = docs.map((x) => migrator.migrateDocument(x));
- expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(1, migratedDocs[0]);
- expect(serializer.savedObjectToRaw).toHaveBeenNthCalledWith(2, migratedDocs[1]);
- });
-
- it(`adds namespace to body when providing namespace for single-namespace type`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace });
- expectMigrationArgs({ namespace }, true, 1);
- expectMigrationArgs({ namespace }, true, 2);
- });
-
- it(`doesn't add namespace to body when providing no namespace for single-namespace type`, async () => {
- await bulkCreateSuccess(client, repository, [obj1, obj2]);
- expectMigrationArgs({ namespace: expect.anything() }, false, 1);
- expectMigrationArgs({ namespace: expect.anything() }, false, 2);
- });
-
- it(`doesn't add namespace to body when not using single-namespace type`, async () => {
- const objects = [
- { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE },
- { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE },
- ];
- await bulkCreateSuccess(client, repository, objects, { namespace });
- expectMigrationArgs({ namespace: expect.anything() }, false, 1);
- expectMigrationArgs({ namespace: expect.anything() }, false, 2);
- });
-
- it(`adds namespaces to body when providing namespace for multi-namespace type`, async () => {
- const objects = [obj1, obj2].map((obj) => ({
- ...obj,
- type: MULTI_NAMESPACE_ISOLATED_TYPE,
- }));
- await bulkCreateSuccess(client, repository, objects, { namespace });
- expectMigrationArgs({ namespaces: [namespace] }, true, 1);
- expectMigrationArgs({ namespaces: [namespace] }, true, 2);
- });
-
- it(`adds default namespaces to body when providing no namespace for multi-namespace type`, async () => {
- const objects = [obj1, obj2].map((obj) => ({
- ...obj,
- type: MULTI_NAMESPACE_ISOLATED_TYPE,
- }));
- await bulkCreateSuccess(client, repository, objects);
- expectMigrationArgs({ namespaces: ['default'] }, true, 1);
- expectMigrationArgs({ namespaces: ['default'] }, true, 2);
- });
-
- it(`doesn't add namespaces to body when not using multi-namespace type`, async () => {
- const objects = [obj1, { ...obj2, type: NAMESPACE_AGNOSTIC_TYPE }];
- await bulkCreateSuccess(client, repository, objects);
- expectMigrationArgs({ namespaces: expect.anything() }, false, 1);
- expectMigrationArgs({ namespaces: expect.anything() }, false, 2);
- });
- });
-
- describe('returns', () => {
- it(`formats the ES response`, async () => {
- const result = await bulkCreateSuccess(client, repository, [obj1, obj2]);
- expect(result).toEqual({
- saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
- });
- });
-
- it.todo(`should return objects in the same order regardless of type`);
-
- it(`handles a mix of successful creates and errors`, async () => {
- const obj = {
- type: 'unknownType',
- id: 'three',
- attributes: {},
- };
- const objects = [obj1, obj, obj2];
- const response = getMockBulkCreateResponse([obj1, obj2]);
- client.bulk.mockResponseOnce(response);
- const result = await repository.bulkCreate(objects);
- expect(client.bulk).toHaveBeenCalledTimes(1);
- expect(result).toEqual({
- saved_objects: [expectCreateResult(obj1), expectError(obj), expectCreateResult(obj2)],
- });
- });
-
- it(`a deserialized saved object`, async () => {
- // Test for fix to https://github.com/elastic/kibana/issues/65088 where
- // we returned raw ID's when an object without an id was created.
- const namespace = 'myspace';
- // FIXME: this test is based on a gigantic hack to have the bulk operation return the source
- // of the document when it actually does not, forcing to cast to any as BulkResponse
- // does not contains _source
- const response = getMockBulkCreateResponse([obj1, obj2], namespace) as any;
- client.bulk.mockResponseOnce(response);
-
- // Bulk create one object with id unspecified, and one with id specified
- const result = await repository.bulkCreate([{ ...obj1, id: undefined }, obj2], {
- namespace,
- });
-
- // Assert that both raw docs from the ES response are deserialized
- expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(
- 1,
- {
- ...response.items[0].create,
- _source: {
- ...response.items[0].create._source,
- namespaces: response.items[0].create._source.namespaces,
- coreMigrationVersion: expect.any(String),
- typeMigrationVersion: '1.1.1',
- },
- _id: expect.stringMatching(
- /^myspace:config:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/
- ),
- },
- expect.any(Object)
- );
- expect(serializer.rawToSavedObject).toHaveBeenNthCalledWith(
- 2,
- {
- ...response.items[1].create,
- _source: {
- ...response.items[1].create._source,
- namespaces: response.items[1].create._source.namespaces,
- coreMigrationVersion: expect.any(String),
- typeMigrationVersion: '1.1.1',
- },
- },
- expect.any(Object)
- );
-
- // Assert that ID's are deserialized to remove the type and namespace
- expect(result.saved_objects[0].id).toEqual(
- expect.stringMatching(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/)
- );
- expect(result.saved_objects[1].id).toEqual(obj2.id);
-
- // Assert that managed is not changed
- expect(result.saved_objects[0].managed).toBeFalsy();
- expect(result.saved_objects[1].managed).toEqual(obj2.managed);
- });
-
- it(`sets managed=false if not already set`, async () => {
- const obj1WithoutManaged = {
- type: 'config',
- id: '6.0.0-alpha1',
- attributes: { title: 'Test One' },
- references: [{ name: 'ref_0', type: 'test', id: '1' }],
- };
- const obj2WithoutManaged = {
- type: 'index-pattern',
- id: 'logstash-*',
- attributes: { title: 'Test Two' },
- references: [{ name: 'ref_0', type: 'test', id: '2' }],
- };
- const result = await bulkCreateSuccess(client, repository, [
- obj1WithoutManaged,
- obj2WithoutManaged,
- ]);
- expect(result).toEqual({
- saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
- });
- });
-
- it(`sets managed=false only on documents without managed already set`, async () => {
- const objWithoutManaged = {
- type: 'config',
- id: '6.0.0-alpha1',
- attributes: { title: 'Test One' },
- references: [{ name: 'ref_0', type: 'test', id: '1' }],
- };
- const result = await bulkCreateSuccess(client, repository, [objWithoutManaged, obj2]);
- expect(result).toEqual({
- saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
- });
- });
-
- it(`sets managed=true if provided as an override`, async () => {
- const obj1WithoutManaged = {
- type: 'config',
- id: '6.0.0-alpha1',
- attributes: { title: 'Test One' },
- references: [{ name: 'ref_0', type: 'test', id: '1' }],
- };
- const obj2WithoutManaged = {
- type: 'index-pattern',
- id: 'logstash-*',
- attributes: { title: 'Test Two' },
- references: [{ name: 'ref_0', type: 'test', id: '2' }],
- };
- const result = await bulkCreateSuccess(
- client,
- repository,
- [obj1WithoutManaged, obj2WithoutManaged],
- { managed: true }
- );
- expect(result).toEqual({
- saved_objects: [
- { ...obj1WithoutManaged, managed: true },
- { ...obj2WithoutManaged, managed: true },
- ].map((x) => expectCreateResult(x)),
- });
- });
-
- it(`sets managed=false if provided as an override`, async () => {
- const obj1WithoutManaged = {
- type: 'config',
- id: '6.0.0-alpha1',
- attributes: { title: 'Test One' },
- references: [{ name: 'ref_0', type: 'test', id: '1' }],
- };
- const obj2WithoutManaged = {
- type: 'index-pattern',
- id: 'logstash-*',
- attributes: { title: 'Test Two' },
- references: [{ name: 'ref_0', type: 'test', id: '2' }],
- };
- const result = await bulkCreateSuccess(
- client,
- repository,
- [obj1WithoutManaged, obj2WithoutManaged],
- { managed: false }
- );
- expect(result).toEqual({
- saved_objects: [obj1, obj2].map((x) => expectCreateResult(x)),
- });
- });
- });
- });
-
describe('#bulkGet', () => {
const obj1: SavedObject = {
type: 'config',
diff --git a/renovate.json b/renovate.json
index e4c1e0672c7829..63ca5d0ba8a972 100644
--- a/renovate.json
+++ b/renovate.json
@@ -462,7 +462,8 @@
"Team:Operations",
"release_note:skip"
],
- "enabled": true
+ "enabled": true,
+ "allowedVersions": "<7.0"
},
{
"groupName": "react-query",
@@ -647,4 +648,4 @@
"enabled": true
}
]
-}
+}
\ No newline at end of file
diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index 98470ccf136580..88d8c04b42337b 100644
--- a/src/dev/storybook/aliases.ts
+++ b/src/dev/storybook/aliases.ts
@@ -15,7 +15,6 @@ export const storybookAliases = {
canvas: 'x-pack/plugins/canvas/storybook',
cases: 'packages/kbn-cases-components/.storybook',
cell_actions: 'packages/kbn-cell-actions/.storybook',
- ci_composite: '.ci/.storybook',
cloud_chat: 'x-pack/plugins/cloud_integrations/cloud_chat/.storybook',
coloring: 'packages/kbn-coloring/.storybook',
language_documentation_popover: 'packages/kbn-language-documentation-popover/.storybook',
diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx
index 5ff2bacaf49fe1..4ad4608080b52c 100644
--- a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx
+++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx
@@ -7,24 +7,24 @@
*/
import classNames from 'classnames';
-import useAsync from 'react-use/lib/useAsync';
import React, { useEffect, useMemo, useState } from 'react';
+import useAsync from 'react-use/lib/useAsync';
import useObservable from 'react-use/lib/useObservable';
-import {
- DashboardDrilldownOptions,
- DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
-} from '@kbn/presentation-util-plugin/public';
import { EuiButtonEmpty, EuiListGroupItem } from '@elastic/eui';
-import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import {
DashboardLocatorParams,
getDashboardLocatorParamsFromEmbeddable,
} from '@kbn/dashboard-plugin/public';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+import {
+ DashboardDrilldownOptions,
+ DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
+} from '@kbn/presentation-util-plugin/public';
-import { LINKS_VERTICAL_LAYOUT, LinksLayoutType, Link } from '../../../common/content_management';
+import { Link, LinksLayoutType, LINKS_VERTICAL_LAYOUT } from '../../../common/content_management';
+import { useLinks } from '../links_hooks';
import { DashboardLinkStrings } from './dashboard_link_strings';
-import { useLinks } from '../../embeddable/links_embeddable';
import { fetchDashboard } from './dashboard_link_tools';
export const DashboardLinkComponent = ({
diff --git a/src/plugins/links/public/components/editor/links_editor.tsx b/src/plugins/links/public/components/editor/links_editor.tsx
index 0fb22efaf85078..12e48e5aa463fb 100644
--- a/src/plugins/links/public/components/editor/links_editor.tsx
+++ b/src/plugins/links/public/components/editor/links_editor.tsx
@@ -7,41 +7,42 @@
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import useMountedState from 'react-use/lib/useMountedState';
import {
- EuiForm,
EuiBadge,
- EuiTitle,
EuiButton,
- EuiSwitch,
- EuiFormRow,
- EuiToolTip,
- EuiFlexItem,
- EuiFlexGroup,
- EuiDroppable,
- EuiDraggable,
- EuiFlyoutBody,
EuiButtonEmpty,
EuiButtonGroup,
- EuiFlyoutFooter,
- EuiFlyoutHeader,
+ EuiButtonGroupOptionProps,
EuiDragDropContext,
euiDragDropReorder,
- EuiButtonGroupOptionProps,
+ EuiDraggable,
+ EuiDroppable,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiForm,
+ EuiFormRow,
+ EuiSwitch,
+ EuiTitle,
+ EuiToolTip,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { LinksLayoutInfo } from '../../embeddable/types';
import {
Link,
LinksLayoutType,
LINKS_HORIZONTAL_LAYOUT,
LINKS_VERTICAL_LAYOUT,
} from '../../../common/content_management';
+import { memoizedGetOrderedLinkList } from '../../editor/links_editor_tools';
+import { openLinkEditorFlyout } from '../../editor/open_link_editor_flyout';
+import { LinksLayoutInfo } from '../../embeddable/types';
import { coreServices } from '../../services/kibana_services';
import { LinksStrings } from '../links_strings';
-import { openLinkEditorFlyout } from '../../editor/open_link_editor_flyout';
-import { memoizedGetOrderedLinkList } from '../../editor/links_editor_tools';
import { LinksEditorEmptyPrompt } from './links_editor_empty_prompt';
import { LinksEditorSingleLink } from './links_editor_single_link';
@@ -80,6 +81,7 @@ const LinksEditor = ({
isByReference: boolean;
}) => {
const toasts = coreServices.notifications.toasts;
+ const isMounted = useMountedState();
const editLinkFlyoutRef = useRef(null);
const [currentLayout, setCurrentLayout] = useState(
@@ -294,7 +296,9 @@ const LinksEditor = ({
});
})
.finally(() => {
- setIsSaving(false);
+ if (isMounted()) {
+ setIsSaving(false);
+ }
});
} else {
onAddToDashboard(orderedLinks, currentLayout);
diff --git a/src/plugins/links/public/components/links_component.tsx b/src/plugins/links/public/components/links_component.tsx
index c72c0db04fd570..0da40365abad0c 100644
--- a/src/plugins/links/public/components/links_component.tsx
+++ b/src/plugins/links/public/components/links_component.tsx
@@ -6,29 +6,28 @@
* Side Public License, v 1.
*/
+import { EuiListGroup, EuiPanel } from '@elastic/eui';
import React, { useEffect, useMemo } from 'react';
import useMap from 'react-use/lib/useMap';
-import { EuiListGroup, EuiPanel } from '@elastic/eui';
-import { useLinks } from '../embeddable/links_embeddable';
-import { ExternalLinkComponent } from './external_link/external_link_component';
-import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
-import { memoizedGetOrderedLinkList } from '../editor/links_editor_tools';
import {
DASHBOARD_LINK_TYPE,
LINKS_HORIZONTAL_LAYOUT,
LINKS_VERTICAL_LAYOUT,
} from '../../common/content_management';
+import { memoizedGetOrderedLinkList } from '../editor/links_editor_tools';
+import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
+import { ExternalLinkComponent } from './external_link/external_link_component';
import './links_component.scss';
+import { useLinks, useLinksAttributes } from './links_hooks';
export const LinksComponent = () => {
const linksEmbeddable = useLinks();
- const links = linksEmbeddable.select((state) => state.componentState.links);
- const layout = linksEmbeddable.select((state) => state.componentState.layout);
+ const linksAttributes = useLinksAttributes();
const [linksLoading, { set: setLinkIsLoading }] = useMap(
Object.fromEntries(
- (links ?? []).map((link) => {
+ (linksAttributes?.links ?? []).map((link) => {
return [link.id, true];
})
)
@@ -43,12 +42,12 @@ export const LinksComponent = () => {
}, [linksLoading, linksEmbeddable]);
const orderedLinks = useMemo(() => {
- if (!links) return [];
- return memoizedGetOrderedLinkList(links);
- }, [links]);
+ if (!linksAttributes?.links) return [];
+ return memoizedGetOrderedLinkList(linksAttributes?.links);
+ }, [linksAttributes]);
const linkItems: { [id: string]: { id: string; content: JSX.Element } } = useMemo(() => {
- return (links ?? []).reduce((prev, currentLink) => {
+ return (linksAttributes?.links ?? []).reduce((prev, currentLink) => {
return {
...prev,
[currentLink.id]: {
@@ -58,7 +57,7 @@ export const LinksComponent = () => {
setLinkIsLoading(currentLink.id, true)}
onRender={() => setLinkIsLoading(currentLink.id, false)}
/>
@@ -66,26 +65,26 @@ export const LinksComponent = () => {
setLinkIsLoading(currentLink.id, false)}
/>
),
},
};
}, {});
- }, [links, layout, setLinkIsLoading]);
+ }, [linksAttributes?.links, linksAttributes?.layout, setLinkIsLoading]);
return (
{orderedLinks.map((link) => linkItems[link.id].content)}
diff --git a/src/plugins/links/public/components/links_hooks.tsx b/src/plugins/links/public/components/links_hooks.tsx
new file mode 100644
index 00000000000000..aa33c9d0f3ac10
--- /dev/null
+++ b/src/plugins/links/public/components/links_hooks.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { useContext, useEffect, useState } from 'react';
+
+import { LinksAttributes } from '../../common/content_management';
+import { LinksContext, LinksEmbeddable } from '../embeddable/links_embeddable';
+
+export const useLinks = (): LinksEmbeddable => {
+ const linksEmbeddable = useContext(LinksContext);
+ if (linksEmbeddable == null) {
+ throw new Error('useLinks must be used inside LinksContext.');
+ }
+ return linksEmbeddable!;
+};
+
+export const useLinksAttributes = (): LinksAttributes | undefined => {
+ const linksEmbeddable = useLinks();
+ const [attributes, setAttributes] = useState(
+ linksEmbeddable.attributes
+ );
+
+ useEffect(() => {
+ const attributesSubscription = linksEmbeddable.attributes$.subscribe((newAttributes) => {
+ setAttributes(newAttributes);
+ });
+ return () => {
+ attributesSubscription.unsubscribe();
+ };
+ }, [linksEmbeddable.attributes$]);
+
+ return attributes;
+};
diff --git a/src/plugins/links/public/embeddable/links_embeddable.tsx b/src/plugins/links/public/embeddable/links_embeddable.tsx
index 032b0099ed4518..d803b9df9e8c5d 100644
--- a/src/plugins/links/public/embeddable/links_embeddable.tsx
+++ b/src/plugins/links/public/embeddable/links_embeddable.tsx
@@ -6,37 +6,25 @@
* Side Public License, v 1.
*/
-import React, { createContext, useContext } from 'react';
-import { unmountComponentAtNode } from 'react-dom';
-import { Subscription, distinctUntilChanged, skip, switchMap } from 'rxjs';
import deepEqual from 'fast-deep-equal';
+import React, { createContext } from 'react';
+import { unmountComponentAtNode } from 'react-dom';
+import { distinctUntilChanged, skip, Subject, Subscription, switchMap } from 'rxjs';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import {
AttributeService,
Embeddable,
ReferenceOrValueEmbeddable,
SavedObjectEmbeddableInput,
} from '@kbn/embeddable-plugin/public';
-import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
-import { linksReducers } from './links_reducers';
-import { LinksByReferenceInput, LinksByValueInput, LinksReduxState } from './types';
-import { LinksComponent } from '../components/links_component';
-import { LinksInput, LinksOutput } from './types';
-import { LinksAttributes } from '../../common/content_management';
import { CONTENT_ID } from '../../common';
+import { LinksAttributes } from '../../common/content_management';
+import { LinksComponent } from '../components/links_component';
+import { LinksByReferenceInput, LinksByValueInput, LinksInput, LinksOutput } from './types';
export const LinksContext = createContext(null);
-export const useLinks = (): LinksEmbeddable => {
- const linksEmbeddable = useContext(LinksContext);
- if (linksEmbeddable == null) {
- throw new Error('useLinks must be used inside LinksContext.');
- }
- return linksEmbeddable!;
-};
-
-type LinksReduxEmbeddableTools = ReduxEmbeddableTools;
export interface LinksConfig {
editable: boolean;
@@ -53,20 +41,10 @@ export class LinksEmbeddable
private isDestroyed?: boolean;
private subscriptions: Subscription = new Subscription();
- // state management
- /**
- * TODO: Keep track of the necessary state without the redux embeddable tools; it's kind of overkill here.
- * Related issue: https://github.com/elastic/kibana/issues/167577
- */
- public select: LinksReduxEmbeddableTools['select'];
- public getState: LinksReduxEmbeddableTools['getState'];
- public dispatch: LinksReduxEmbeddableTools['dispatch'];
- public onStateChange: LinksReduxEmbeddableTools['onStateChange'];
-
- private cleanupStateTools: () => void;
+ public attributes?: LinksAttributes;
+ public attributes$ = new Subject();
constructor(
- reduxToolsPackage: ReduxToolsPackage,
config: LinksConfig,
initialInput: LinksInput,
private attributeService: AttributeService,
@@ -81,29 +59,11 @@ export class LinksEmbeddable
parent
);
- /** Build redux embeddable tools */
- const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
- LinksReduxState,
- typeof linksReducers
- >({
- embeddable: this,
- reducers: linksReducers,
- initialComponentState: {
- title: '',
- },
- });
-
- this.select = reduxEmbeddableTools.select;
- this.getState = reduxEmbeddableTools.getState;
- this.dispatch = reduxEmbeddableTools.dispatch;
- this.cleanupStateTools = reduxEmbeddableTools.cleanup;
- this.onStateChange = reduxEmbeddableTools.onStateChange;
-
this.initializeSavedLinks()
.then(() => this.setInitializationFinished())
.catch((e: Error) => this.onFatalError(e));
- // By-value panels should update the componentState when input changes
+ // By-value panels should update the links attributes when input changes
this.subscriptions.add(
this.getInput$()
.pipe(
@@ -113,25 +73,28 @@ export class LinksEmbeddable
)
.subscribe()
);
+
+ // Keep attributes in sync with subject value so it can be used in output
+ this.subscriptions.add(
+ this.attributes$.pipe(distinctUntilChanged(deepEqual)).subscribe((attributes) => {
+ this.attributes = attributes;
+ })
+ );
}
private async initializeSavedLinks() {
const { attributes } = await this.attributeService.unwrapAttributes(this.getInput());
- if (this.isDestroyed) return;
-
- this.dispatch.setAttributes(attributes);
-
+ this.attributes$.next(attributes);
await this.initializeOutput();
}
private async initializeOutput() {
- const attributes = this.getState().componentState;
const { title, description } = this.getInput();
this.updateOutput({
- defaultTitle: attributes.title,
- defaultDescription: attributes.description,
- title: title ?? attributes.title,
- description: description ?? attributes.description,
+ defaultTitle: this.attributes?.title,
+ defaultDescription: this.attributes?.description,
+ title: title ?? this.attributes?.title,
+ description: description ?? this.attributes?.description,
});
}
@@ -162,7 +125,7 @@ export class LinksEmbeddable
public async reload() {
if (this.isDestroyed) return;
- // By-reference embeddable panels are reloaded when changed, so update the componentState
+ // By-reference embeddable panels are reloaded when changed, so update the attributes
this.initializeSavedLinks();
if (this.domNode) {
this.render(this.domNode);
@@ -173,7 +136,6 @@ export class LinksEmbeddable
this.isDestroyed = true;
super.destroy();
this.subscriptions.unsubscribe();
- this.cleanupStateTools();
if (this.domNode) {
unmountComponentAtNode(this.domNode);
}
diff --git a/src/plugins/links/public/embeddable/links_embeddable_factory.ts b/src/plugins/links/public/embeddable/links_embeddable_factory.ts
index e0502d34a742c3..55838c6d652296 100644
--- a/src/plugins/links/public/embeddable/links_embeddable_factory.ts
+++ b/src/plugins/links/public/embeddable/links_embeddable_factory.ts
@@ -6,34 +6,33 @@
* Side Public License, v 1.
*/
+import { DASHBOARD_GRID_COLUMN_COUNT } from '@kbn/dashboard-plugin/public';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+import { IProvidesPanelPlacementSettings } from '@kbn/dashboard-plugin/public/dashboard_container/component/panel_placement/types';
+import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import {
EmbeddableFactory,
EmbeddableFactoryDefinition,
ErrorEmbeddable,
} from '@kbn/embeddable-plugin/public';
import {
- MigrateFunctionsObject,
GetMigrationFunctionObjectFn,
+ MigrateFunctionsObject,
} from '@kbn/kibana-utils-plugin/common';
-import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
-import { DASHBOARD_GRID_COLUMN_COUNT } from '@kbn/dashboard-plugin/public';
import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public';
-import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
-import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { IProvidesPanelPlacementSettings } from '@kbn/dashboard-plugin/public/dashboard_container/component/panel_placement/types';
+import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
+import { LinksAttributes } from '../../common/content_management';
+import { extract, inject } from '../../common/embeddable';
+import { LinksStrings } from '../components/links_strings';
+import { getLinksAttributeService } from '../services/attribute_service';
import {
coreServices,
presentationUtil,
untilPluginStartServicesReady,
} from '../services/kibana_services';
-import { extract, inject } from '../../common/embeddable';
import type { LinksEmbeddable } from './links_embeddable';
-import { LinksStrings } from '../components/links_strings';
-import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
-import { LinksAttributes } from '../../common/content_management';
-import { getLinksAttributeService } from '../services/attribute_service';
-import { LinksInput, LinksByReferenceInput, LinksEditorFlyoutReturn } from './types';
+import { LinksByReferenceInput, LinksEditorFlyoutReturn, LinksInput } from './types';
export type LinksFactory = EmbeddableFactory;
@@ -111,12 +110,10 @@ export class LinksFactoryDefinition
public async create(initialInput: LinksInput, parent: DashboardContainer) {
await untilPluginStartServicesReady();
- const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const { LinksEmbeddable } = await import('./links_embeddable');
const editable = await this.isEditable();
return new LinksEmbeddable(
- reduxEmbeddablePackage,
{ editable },
{ ...getDefaultLinksInput(), ...initialInput },
getLinksAttributeService(),
diff --git a/src/plugins/links/public/embeddable/links_reducers.ts b/src/plugins/links/public/embeddable/links_reducers.ts
deleted file mode 100644
index 659b19058adbb8..00000000000000
--- a/src/plugins/links/public/embeddable/links_reducers.ts
+++ /dev/null
@@ -1,27 +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 { WritableDraft } from 'immer/dist/types/types-external';
-
-import { PayloadAction } from '@reduxjs/toolkit';
-
-import { LinksReduxState } from './types';
-import { LinksAttributes } from '../../common/content_management';
-
-export const linksReducers = {
- setLoading: (state: WritableDraft, action: PayloadAction) => {
- state.output.loading = action.payload;
- },
-
- setAttributes: (
- state: WritableDraft,
- action: PayloadAction
- ) => {
- state.componentState = { ...action.payload };
- },
-};
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 06ade5904efadd..2d9419b2b7712e 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1066,6 +1066,8 @@
"@kbn/ml-string-hash/*": ["x-pack/packages/ml/string_hash/*"],
"@kbn/ml-trained-models-utils": ["x-pack/packages/ml/trained_models_utils"],
"@kbn/ml-trained-models-utils/*": ["x-pack/packages/ml/trained_models_utils/*"],
+ "@kbn/ml-ui-actions": ["x-pack/packages/ml/ui_actions"],
+ "@kbn/ml-ui-actions/*": ["x-pack/packages/ml/ui_actions/*"],
"@kbn/ml-url-state": ["x-pack/packages/ml/url_state"],
"@kbn/ml-url-state/*": ["x-pack/packages/ml/url_state/*"],
"@kbn/monaco": ["packages/kbn-monaco"],
diff --git a/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx b/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx
index 5b2a9d880c1b8e..cb890172bbdd9f 100644
--- a/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx
+++ b/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx
@@ -7,7 +7,7 @@
import React, { FC, useCallback, useMemo, useState } from 'react';
-import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import {
EuiButton,
diff --git a/x-pack/packages/ml/date_picker/src/services/full_time_range_selector_service.ts b/x-pack/packages/ml/date_picker/src/services/full_time_range_selector_service.ts
index cfd363e8b53ce2..c4cdd23995ae77 100644
--- a/x-pack/packages/ml/date_picker/src/services/full_time_range_selector_service.ts
+++ b/x-pack/packages/ml/date_picker/src/services/full_time_range_selector_service.ts
@@ -8,7 +8,7 @@
import moment from 'moment';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import dateMath from '@kbn/datemath';
-import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { ToastsStart, HttpStart } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/public';
diff --git a/x-pack/packages/ml/date_picker/src/services/time_field_range.ts b/x-pack/packages/ml/date_picker/src/services/time_field_range.ts
index 07a5fb3de3553e..92d71f582a1ef6 100644
--- a/x-pack/packages/ml/date_picker/src/services/time_field_range.ts
+++ b/x-pack/packages/ml/date_picker/src/services/time_field_range.ts
@@ -6,7 +6,7 @@
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { HttpStart } from '@kbn/core/public';
diff --git a/x-pack/packages/ml/query_utils/src/add_exclude_frozen_to_query.ts b/x-pack/packages/ml/query_utils/src/add_exclude_frozen_to_query.ts
index 71625460349641..3148e93bed02f0 100644
--- a/x-pack/packages/ml/query_utils/src/add_exclude_frozen_to_query.ts
+++ b/x-pack/packages/ml/query_utils/src/add_exclude_frozen_to_query.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { cloneDeep } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
diff --git a/x-pack/packages/ml/ui_actions/README.md b/x-pack/packages/ml/ui_actions/README.md
new file mode 100644
index 00000000000000..a9cba4a278c8ea
--- /dev/null
+++ b/x-pack/packages/ml/ui_actions/README.md
@@ -0,0 +1,3 @@
+# @kbn/ml-ui-actions
+
+Empty package generated by @kbn/generate
diff --git a/x-pack/packages/ml/ui_actions/index.ts b/x-pack/packages/ml/ui_actions/index.ts
new file mode 100644
index 00000000000000..8077406b586754
--- /dev/null
+++ b/x-pack/packages/ml/ui_actions/index.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.
+ */
+
+export {
+ CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION,
+ CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
+ type CreateCategorizationADJobContext,
+} from './src/ui_actions';
diff --git a/x-pack/packages/ml/ui_actions/jest.config.js b/x-pack/packages/ml/ui_actions/jest.config.js
new file mode 100644
index 00000000000000..8efadb42705fa8
--- /dev/null
+++ b/x-pack/packages/ml/ui_actions/jest.config.js
@@ -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.
+ */
+
+module.exports = {
+ preset: '@kbn/test/jest_node',
+ rootDir: '../../../..',
+ roots: ['/x-pack/packages/ml/ui_actions'],
+};
diff --git a/x-pack/packages/ml/ui_actions/kibana.jsonc b/x-pack/packages/ml/ui_actions/kibana.jsonc
new file mode 100644
index 00000000000000..999f955bc2e471
--- /dev/null
+++ b/x-pack/packages/ml/ui_actions/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-common",
+ "id": "@kbn/ml-ui-actions",
+ "owner": "@elastic/ml-ui"
+}
diff --git a/x-pack/packages/ml/ui_actions/package.json b/x-pack/packages/ml/ui_actions/package.json
new file mode 100644
index 00000000000000..a3d77f4701c68d
--- /dev/null
+++ b/x-pack/packages/ml/ui_actions/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/ml-ui-actions",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0"
+}
\ No newline at end of file
diff --git a/x-pack/packages/ml/ui_actions/src/ui_actions.ts b/x-pack/packages/ml/ui_actions/src/ui_actions.ts
new file mode 100644
index 00000000000000..cf36e3ff01ce66
--- /dev/null
+++ b/x-pack/packages/ml/ui_actions/src/ui_actions.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
+import type { TimeRange } from '@kbn/es-query';
+
+export interface CreateCategorizationADJobContext {
+ field: DataViewField;
+ dataView: DataView;
+ query: QueryDslQueryContainer;
+ timeRange: TimeRange;
+}
+
+export const CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION = 'createMLADCategorizationJobAction';
+
+export const CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER =
+ 'CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER';
diff --git a/x-pack/packages/ml/ui_actions/tsconfig.json b/x-pack/packages/ml/ui_actions/tsconfig.json
new file mode 100644
index 00000000000000..25ae95b59b1638
--- /dev/null
+++ b/x-pack/packages/ml/ui_actions/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/data-views-plugin",
+ "@kbn/es-query",
+ ]
+}
diff --git a/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts b/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts
index f65292a2919e40..1ad08a9332503a 100644
--- a/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts
+++ b/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts
@@ -95,7 +95,7 @@ export const dataTableReducer = reducerWithInitialState(initialDataTableState)
[action.id]: {
...state.tableById[action.id],
expandedDetail: {
- ...state.tableById[action.id].expandedDetail,
+ ...state.tableById[action.id]?.expandedDetail,
...updateTableDetailsPanel(action),
},
},
diff --git a/x-pack/plugins/aiops/public/components/log_categorization/create_categorization_job.tsx b/x-pack/plugins/aiops/public/components/log_categorization/create_categorization_job.tsx
new file mode 100644
index 00000000000000..66458c511d981a
--- /dev/null
+++ b/x-pack/plugins/aiops/public/components/log_categorization/create_categorization_job.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+
+import moment from 'moment';
+import { EuiButtonEmpty } from '@elastic/eui';
+import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import {
+ CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
+ type CreateCategorizationADJobContext,
+} from '@kbn/ml-ui-actions';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
+
+interface Props {
+ dataView: DataView;
+ field: DataViewField;
+ query: QueryDslQueryContainer;
+ earliest: number | undefined;
+ latest: number | undefined;
+}
+
+export const CreateCategorizationJobButton: FC = ({
+ dataView,
+ field,
+ query,
+ earliest,
+ latest,
+}) => {
+ const {
+ uiActions,
+ application: { capabilities },
+ } = useAiopsAppContext();
+
+ const createADJob = () => {
+ if (uiActions === undefined) {
+ return;
+ }
+
+ const triggerOptions: CreateCategorizationADJobContext = {
+ dataView,
+ field,
+ query,
+ timeRange: { from: moment(earliest).toISOString(), to: moment(latest).toISOString() },
+ };
+ uiActions.getTrigger(CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER).exec(triggerOptions);
+ };
+
+ if (uiActions === undefined || capabilities.ml.canCreateJob === false) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx
index a5262393e0eec8..5b37fd019c013c 100644
--- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx
+++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx
@@ -44,6 +44,7 @@ import { TechnicalPreviewBadge } from './technical_preview_badge';
import { LoadingCategorization } from './loading_categorization';
import { useValidateFieldRequest } from './use_validate_category_field';
import { FieldValidationCallout } from './category_validation_callout';
+import { CreateCategorizationJobButton } from './create_categorization_job';
export interface LogCategorizationPageProps {
dataView: DataView;
@@ -261,17 +262,21 @@ export const LogCategorizationFlyout: FC = ({
+
-
{loading === true ? : null}
-
-
{loading === false && data !== null && data.categories.length > 0 ? (
{
return { coreSetup, plugin };
};
- it('registers help support URL', async () => {
- const { plugin } = startPlugin();
+ it('registers help support URL: default', async () => {
+ const { plugin } = startPlugin({
+ id: undefined,
+ });
const coreStart = coreMock.createStart();
plugin.start(coreStart);
@@ -211,6 +213,41 @@ describe('Cloud Plugin', () => {
`);
});
+ it('registers help support URL: serverless projects', async () => {
+ const { plugin } = startPlugin({
+ id: 'my-awesome-project-id',
+ serverless: {
+ project_id: 'my-awesome-serverless-project-id',
+ },
+ });
+
+ const coreStart = coreMock.createStart();
+ plugin.start(coreStart);
+
+ expect(coreStart.chrome.setHelpSupportUrl).toHaveBeenCalledTimes(1);
+ expect(coreStart.chrome.setHelpSupportUrl.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "https://support.elastic.co/?serverless_project_id=my-awesome-serverless-project-id",
+ ]
+ `);
+ });
+
+ it('registers help support URL: non-serverless projects', async () => {
+ const { plugin } = startPlugin({
+ id: 'my-awesome-project-id',
+ });
+
+ const coreStart = coreMock.createStart();
+ plugin.start(coreStart);
+
+ expect(coreStart.chrome.setHelpSupportUrl).toHaveBeenCalledTimes(1);
+ expect(coreStart.chrome.setHelpSupportUrl.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "https://support.elastic.co/?cloud_deployment_id=my-awesome-project-id",
+ ]
+ `);
+ });
+
describe('isServerlessEnabled', () => {
it('is `true` when `serverless.projectId` is set', () => {
const { plugin } = startPlugin({
diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx
index 3aaaf8f14fe271..f0e7b8f713e807 100644
--- a/x-pack/plugins/cloud/public/plugin.tsx
+++ b/x-pack/plugins/cloud/public/plugin.tsx
@@ -11,10 +11,11 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb
import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { parseDeploymentIdFromDeploymentUrl } from '../common/parse_deployment_id_from_deployment_url';
-import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants';
+import { CLOUD_SNAPSHOTS_PATH } from '../common/constants';
import { decodeCloudId, type DecodedCloudId } from '../common/decode_cloud_id';
import type { CloudSetup, CloudStart } from './types';
import { getFullCloudUrl } from '../common/utils';
+import { getSupportUrl } from './utils';
export interface CloudConfigType {
id?: string;
@@ -103,7 +104,7 @@ export class CloudPlugin implements Plugin {
}
public start(coreStart: CoreStart): CloudStart {
- coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
+ coreStart.chrome.setHelpSupportUrl(getSupportUrl(this.config));
// Nest all the registered context providers under the Cloud Services Provider.
// This way, plugins only need to require Cloud's context provider to have all the enriched Cloud services.
diff --git a/x-pack/plugins/cloud/public/utils.ts b/x-pack/plugins/cloud/public/utils.ts
new file mode 100644
index 00000000000000..d2381c428fe0a9
--- /dev/null
+++ b/x-pack/plugins/cloud/public/utils.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { ELASTIC_SUPPORT_LINK } from '../common/constants';
+import { CloudConfigType } from './plugin';
+
+export function getSupportUrl(config: CloudConfigType): string {
+ let supportUrl = ELASTIC_SUPPORT_LINK;
+ if (config.serverless?.project_id) {
+ // serverless projects use config.id and config.serverless.project_id
+ supportUrl += '?serverless_project_id=' + config.serverless.project_id;
+ } else if (config.id) {
+ // non-serverless Cloud projects only use config.id
+ supportUrl += '?cloud_deployment_id=' + config.id;
+ }
+ return supportUrl;
+}
diff --git a/x-pack/plugins/ml/common/constants/locator.ts b/x-pack/plugins/ml/common/constants/locator.ts
index 92c89c5aedf94a..614c037c13026c 100644
--- a/x-pack/plugins/ml/common/constants/locator.ts
+++ b/x-pack/plugins/ml/common/constants/locator.ts
@@ -55,6 +55,7 @@ export const ML_PAGES = {
ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: 'jobs/new_job/step/job_type',
ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: 'jobs/new_job/step/index_or_search',
ANOMALY_DETECTION_CREATE_JOB_FROM_LENS: 'jobs/new_job/from_lens',
+ ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS: 'jobs/new_job/from_pattern_analysis',
ANOMALY_DETECTION_CREATE_JOB_FROM_MAP: 'jobs/new_job/from_map',
ANOMALY_DETECTION_MODULES_VIEW_OR_CREATE: 'modules/check_view_or_create',
SETTINGS: 'settings',
diff --git a/x-pack/plugins/ml/common/constants/new_job.ts b/x-pack/plugins/ml/common/constants/new_job.ts
index 24584a6e8d29b2..676182d0ac162d 100644
--- a/x-pack/plugins/ml/common/constants/new_job.ts
+++ b/x-pack/plugins/ml/common/constants/new_job.ts
@@ -26,6 +26,7 @@ export enum CREATED_BY_LABEL {
APM_TRANSACTION = 'ml-module-apm-transaction',
SINGLE_METRIC_FROM_LENS = 'single-metric-wizard-from-lens',
MULTI_METRIC_FROM_LENS = 'multi-metric-wizard-from-lens',
+ CATEGORIZATION_FROM_PATTERN_ANALYSIS = 'categorization-wizard-from-pattern-analysis',
}
export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB';
diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts
index 329ba59ba90733..85b2550eb8e307 100644
--- a/x-pack/plugins/ml/common/types/locator.ts
+++ b/x-pack/plugins/ml/common/types/locator.ts
@@ -49,6 +49,7 @@ export type MlGenericUrlState = MLPageState<
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_LENS
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_MAP
+ | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB
diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts
index 70f588712c3518..95815cd3b87af0 100644
--- a/x-pack/plugins/ml/common/types/trained_models.ts
+++ b/x-pack/plugins/ml/common/types/trained_models.ts
@@ -98,6 +98,7 @@ export type TrainedModelConfigResponse = estypes.MlTrainedModelConfig & {
* Associated pipelines. Extends response from the ES endpoint.
*/
pipelines?: Record | null;
+ origin_job_exists?: boolean;
metadata?: {
analytics_config: DataFrameAnalyticsConfig;
diff --git a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx
index 3919f9f9e5ccd8..70c3be96c66742 100644
--- a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx
+++ b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx
@@ -65,6 +65,7 @@ export const ChangePointDetectionPage: FC = () => {
'share',
'storage',
'theme',
+ 'uiActions',
'uiSettings',
'unifiedSearch',
'usageCollection',
diff --git a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx
index 455ff9bfc13774..187843505cef7e 100644
--- a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx
+++ b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx
@@ -56,6 +56,7 @@ export const LogCategorizationPage: FC = () => {
'share',
'storage',
'theme',
+ 'uiActions',
'uiSettings',
'unifiedSearch',
])}
diff --git a/x-pack/plugins/ml/public/application/aiops/log_rate_analysis.tsx b/x-pack/plugins/ml/public/application/aiops/log_rate_analysis.tsx
index c20264a129ea31..4c2c1dfd637dbc 100644
--- a/x-pack/plugins/ml/public/application/aiops/log_rate_analysis.tsx
+++ b/x-pack/plugins/ml/public/application/aiops/log_rate_analysis.tsx
@@ -59,6 +59,7 @@ export const LogRateAnalysisPage: FC = () => {
'share',
'storage',
'theme',
+ 'uiActions',
'uiSettings',
'unifiedSearch',
])}
diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.tsx
index 556dd5c810d842..eb5450d0d61bff 100644
--- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.tsx
+++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.tsx
@@ -361,7 +361,7 @@ const CategoryExamples: FC<{ definition: CategoryDefinition; examples: string[]
{definition !== undefined && definition.terms && (
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts
index 4b4ce729927756..f5f491426b0a3a 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts
@@ -69,21 +69,21 @@ export class QuickJobCreatorBase {
datafeedConfig,
jobConfig,
createdByLabel,
- dashboard,
start,
end,
startJob,
runInRealTime,
+ dashboard,
}: {
jobId: string;
datafeedConfig: Datafeed;
jobConfig: Job;
createdByLabel: CREATED_BY_LABEL;
- dashboard: Dashboard;
start: number | undefined;
end: number | undefined;
startJob: boolean;
runInRealTime: boolean;
+ dashboard?: Dashboard;
}) {
const datafeedId = createDatafeedId(jobId);
const datafeed = { ...datafeedConfig, job_id: jobId, datafeed_id: datafeedId };
@@ -93,7 +93,7 @@ export class QuickJobCreatorBase {
job_id: jobId,
custom_settings: {
created_by: createdByLabel,
- ...(await this.getCustomUrls(dashboard, datafeed)),
+ ...(dashboard ? await this.getCustomUrls(dashboard, datafeed) : {}),
},
};
@@ -230,7 +230,7 @@ export class QuickJobCreatorBase {
return mergedQueries;
}
- protected async createDashboardLink(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
+ private async createDashboardLink(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
const dashboardTitle = dashboard?.getTitle();
if (dashboardTitle === undefined || dashboardTitle === '') {
// embeddable may have not been in a dashboard
@@ -274,7 +274,7 @@ export class QuickJobCreatorBase {
return { url_name: urlName, url_value: url, time_range: 'auto' };
}
- protected async getCustomUrls(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
+ private async getCustomUrls(dashboard: Dashboard, datafeedConfig: estypes.MlDatafeed) {
const customUrls = await this.createDashboardLink(dashboard, datafeedConfig);
return dashboard !== undefined && customUrls !== null ? { custom_urls: [customUrls] } : {};
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts
index 2ade08c3cf23d4..4df2b74347f47c 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/quick_create_job.ts
@@ -135,7 +135,7 @@ export class QuickLensJobCreator extends QuickJobCreatorBase {
}
}
- async createJob(
+ private async createJob(
chartInfo: ChartInfo,
startString: string,
endString: string,
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts
index 9dcce1facf2989..c8ad1ee6942e49 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/route_resolver.ts
@@ -15,7 +15,7 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { QuickLensJobCreator } from './quick_create_job';
import type { MlApiServices } from '../../../services/ml_api_service';
-import { getDefaultQuery } from '../utils/new_job_utils';
+import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils';
interface Dependencies {
lens: LensPublicStart;
@@ -27,8 +27,8 @@ interface Dependencies {
export async function resolver(
deps: Dependencies,
lensSavedObjectRisonString: string | undefined,
- fromRisonStrong: string,
- toRisonStrong: string,
+ fromRisonString: string,
+ toRisonString: string,
queryRisonString: string,
filtersRisonString: string,
layerIndexRisonString: string
@@ -43,37 +43,11 @@ export async function resolver(
throw new Error('Cannot create visualization');
}
- let query: Query;
- let filters: Filter[];
- try {
- query = rison.decode(queryRisonString) as Query;
- } catch (error) {
- query = getDefaultQuery();
- }
- try {
- filters = rison.decode(filtersRisonString) as Filter[];
- } catch (error) {
- filters = [];
- }
-
- let from: string;
- let to: string;
- try {
- from = rison.decode(fromRisonStrong) as string;
- } catch (error) {
- from = '';
- }
- try {
- to = rison.decode(toRisonStrong) as string;
- } catch (error) {
- to = '';
- }
- let layerIndex: number | undefined;
- try {
- layerIndex = rison.decode(layerIndexRisonString) as number;
- } catch (error) {
- layerIndex = undefined;
- }
+ const query = getRisonValue(queryRisonString, getDefaultQuery()) as Query;
+ const filters = getRisonValue(filtersRisonString, []);
+ const from = getRisonValue(fromRisonString, '');
+ const to = getRisonValue(toRisonString, '');
+ const layerIndex = getRisonValue(layerIndexRisonString, undefined);
const jobCreator = new QuickLensJobCreator(
lens,
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts
index e590600cd40470..954abd2d14abce 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/quick_create_job.ts
@@ -162,7 +162,7 @@ export class QuickGeoJobCreator extends QuickJobCreatorBase {
}
}
- async createGeoJob({
+ private async createGeoJob({
dataViewId,
sourceDataView,
from,
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts
index 39753c77038ca5..0728ca3c61d59e 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_map/route_resolver.ts
@@ -5,16 +5,13 @@
* 2.0.
*/
-import rison from '@kbn/rison';
-import type { Query } from '@kbn/es-query';
-import type { Filter } from '@kbn/es-query';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { MlApiServices } from '../../../services/ml_api_service';
import { QuickGeoJobCreator } from './quick_create_job';
-import { getDefaultQuery } from '../utils/new_job_utils';
+import { getDefaultQuery, getRisonValue } from '../utils/new_job_utils';
interface Dependencies {
kibanaConfig: IUiSettingsClient;
@@ -24,66 +21,32 @@ interface Dependencies {
}
export async function resolver(
deps: Dependencies,
- dashboard: string,
- dataViewId: string,
- embeddable: string,
- geoField: string,
- splitField: string,
+ dashboardRisonString: string,
+ dataViewIdRisonString: string,
+ embeddableRisonString: string,
+ geoFieldRisonString: string,
+ splitFieldRisonString: string,
fromRisonString: string,
toRisonString: string,
- layer?: string
+ layerRisonString?: string
) {
const { kibanaConfig, timeFilter, dashboardService, mlApiServices } = deps;
- let decodedDashboard;
- let decodedEmbeddable;
- let decodedLayer;
- let splitFieldDecoded;
- let dvId;
+ const defaultLayer = { query: getDefaultQuery(), filters: [] };
- try {
- dvId = rison.decode(dataViewId) as string;
- } catch (error) {
- dvId = '';
- }
+ const dashboard = getRisonValue(dashboardRisonString, defaultLayer);
+ const embeddable = getRisonValue(embeddableRisonString, defaultLayer);
- try {
- decodedDashboard = rison.decode(dashboard) as { query: Query; filters: Filter[] };
- } catch (error) {
- decodedDashboard = { query: getDefaultQuery(), filters: [] };
- }
+ const layer =
+ layerRisonString !== undefined
+ ? getRisonValue(layerRisonString, defaultLayer)
+ : defaultLayer;
- try {
- decodedEmbeddable = rison.decode(embeddable) as { query: Query; filters: Filter[] };
- } catch (error) {
- decodedEmbeddable = { query: getDefaultQuery(), filters: [] };
- }
+ const geoField = getRisonValue(geoFieldRisonString, '');
+ const splitField = getRisonValue(splitFieldRisonString, null);
+ const dataViewId = getRisonValue(dataViewIdRisonString, '');
- if (layer) {
- try {
- decodedLayer = rison.decode(layer) as { query: Query };
- } catch (error) {
- decodedLayer = { query: getDefaultQuery(), filters: [] };
- }
- }
-
- try {
- splitFieldDecoded = rison.decode(splitField) as string;
- } catch (error) {
- splitFieldDecoded = null;
- }
-
- let from: string;
- let to: string;
- try {
- from = rison.decode(fromRisonString) as string;
- } catch (error) {
- from = '';
- }
- try {
- to = rison.decode(toRisonString) as string;
- } catch (error) {
- to = '';
- }
+ const from = getRisonValue(fromRisonString, '');
+ const to = getRisonValue(toRisonString, '');
const jobCreator = new QuickGeoJobCreator(
kibanaConfig,
@@ -93,15 +56,15 @@ export async function resolver(
);
await jobCreator.createAndStashGeoJob(
- dvId,
+ dataViewId,
from,
to,
- decodedDashboard.query,
- decodedDashboard.filters,
- decodedEmbeddable.query,
- decodedEmbeddable.filters,
+ dashboard.query,
+ dashboard.filters,
+ embeddable.query,
+ embeddable.filters,
geoField,
- splitFieldDecoded,
- decodedLayer?.query
+ splitField,
+ layer?.query
);
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/index.ts
new file mode 100644
index 00000000000000..51b3194f28c956
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export {
+ QuickCategorizationJobCreator,
+ CATEGORIZATION_TYPE,
+ type CategorizationType,
+} from './quick_create_job';
+
+export { resolver } from './route_resolver';
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/quick_create_job.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/quick_create_job.ts
new file mode 100644
index 00000000000000..721d48d1908b6e
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/quick_create_job.ts
@@ -0,0 +1,209 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { IUiSettingsClient } from '@kbn/core/public';
+import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
+import type { DashboardStart } from '@kbn/dashboard-plugin/public';
+import { DataViewField, DataView } from '@kbn/data-views-plugin/common';
+import type { TimeRange } from '@kbn/es-query';
+import { i18n } from '@kbn/i18n';
+import { MLCATEGORY, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { CREATED_BY_LABEL, DEFAULT_BUCKET_SPAN } from '../../../../../common/constants/new_job';
+import { type CreateState, QuickJobCreatorBase } from '../job_from_dashboard/quick_create_job_base';
+import type { MlApiServices } from '../../../services/ml_api_service';
+import { createEmptyDatafeed, createEmptyJob } from '../common/job_creator/util/default_configs';
+import { stashJobForCloning } from '../common/job_creator/util/general';
+import type { JobCreatorType } from '../common/job_creator';
+
+export const CATEGORIZATION_TYPE = {
+ COUNT: ML_JOB_AGGREGATION.COUNT,
+ RARE: ML_JOB_AGGREGATION.RARE,
+} as const;
+
+export type CategorizationType = typeof CATEGORIZATION_TYPE[keyof typeof CATEGORIZATION_TYPE];
+
+export class QuickCategorizationJobCreator extends QuickJobCreatorBase {
+ constructor(
+ kibanaConfig: IUiSettingsClient,
+ timeFilter: TimefilterContract,
+ dashboardService: DashboardStart,
+ private data: DataPublicPluginStart,
+ mlApiServices: MlApiServices
+ ) {
+ super(kibanaConfig, timeFilter, dashboardService, mlApiServices);
+ }
+
+ public async createAndSaveJob(
+ categorizationType: CategorizationType,
+ jobId: string,
+ bucketSpan: string,
+ dataView: DataView,
+ field: DataViewField,
+ partitionField: DataViewField | null,
+ stopOnWarn: boolean,
+ query: QueryDslQueryContainer,
+ timeRange: TimeRange,
+ startJob: boolean,
+ runInRealTime: boolean
+ ): Promise {
+ if (query === undefined) {
+ throw new Error('Cannot create job, query and filters are undefined');
+ }
+
+ const { jobConfig, datafeedConfig, start, end } = await this.createJob(
+ categorizationType,
+ dataView,
+ field,
+ partitionField,
+ stopOnWarn,
+ timeRange,
+ query,
+ bucketSpan
+ );
+ const createdByLabel = CREATED_BY_LABEL.CATEGORIZATION_FROM_PATTERN_ANALYSIS;
+
+ const result = await this.putJobAndDataFeed({
+ jobId,
+ datafeedConfig,
+ jobConfig,
+ createdByLabel,
+ start,
+ end,
+ startJob,
+ runInRealTime,
+ });
+ return result;
+ }
+
+ public async createAndStashADJob(
+ categorizationType: CategorizationType,
+ dataViewId: string,
+ fieldName: string,
+ partitionFieldName: string | null,
+ stopOnWarn: boolean,
+ startString: string,
+ endString: string,
+ query: QueryDslQueryContainer
+ ) {
+ try {
+ const dataView = await this.data.dataViews.get(dataViewId);
+ const field = dataView.getFieldByName(fieldName);
+ const partitionField = partitionFieldName
+ ? dataView.getFieldByName(partitionFieldName) ?? null
+ : null;
+
+ if (field === undefined) {
+ throw new Error('Cannot create job, field is undefined');
+ }
+
+ const { jobConfig, datafeedConfig, start, end, includeTimeRange } = await this.createJob(
+ categorizationType,
+ dataView,
+ field,
+ partitionField,
+ stopOnWarn,
+ { from: startString, to: endString },
+ query,
+ DEFAULT_BUCKET_SPAN
+ );
+
+ // add job config and start and end dates to the
+ // job cloning stash, so they can be used
+ // by the new job wizards
+ stashJobForCloning(
+ {
+ jobConfig,
+ datafeedConfig,
+ createdBy: CREATED_BY_LABEL.CATEGORIZATION_FROM_PATTERN_ANALYSIS,
+ start,
+ end,
+ } as JobCreatorType,
+ true,
+ includeTimeRange,
+ !includeTimeRange
+ );
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ }
+ }
+
+ private async createJob(
+ categorizationType: CategorizationType,
+ dataView: DataView,
+ field: DataViewField,
+ partitionField: DataViewField | null,
+ stopOnWarn: boolean,
+ timeRange: TimeRange,
+ query: QueryDslQueryContainer,
+ bucketSpan: string
+ ) {
+ const jobConfig = createEmptyJob();
+ const datafeedConfig = createEmptyDatafeed(dataView.getIndexPattern());
+
+ datafeedConfig.query = query;
+ jobConfig.analysis_config = {
+ categorization_field_name: field.name,
+ per_partition_categorization: {
+ enabled: partitionField !== null,
+ stop_on_warn: stopOnWarn,
+ },
+ influencers: [MLCATEGORY],
+ detectors: [
+ {
+ function: categorizationType,
+ by_field_name: MLCATEGORY,
+ },
+ ],
+ bucket_span: bucketSpan,
+ };
+
+ if (partitionField !== null) {
+ jobConfig.analysis_config.detectors[0].partition_field_name = partitionField.name;
+ jobConfig.analysis_config.influencers!.push(partitionField.name);
+ }
+
+ jobConfig.data_description.time_field = dataView.timeFieldName;
+
+ let start: number | undefined;
+ let end: number | undefined;
+ let includeTimeRange = true;
+
+ try {
+ // attempt to parse the start and end dates.
+ // if start and end values cannot be determined
+ // instruct the job cloning code to auto-select the
+ // full time range for the index.
+ const { min, max } = this.timeFilter.calculateBounds(timeRange);
+ start = min?.valueOf();
+ end = max?.valueOf();
+
+ if (start === undefined || end === undefined || isNaN(start) || isNaN(end)) {
+ throw Error(
+ i18n.translate('xpack.ml.newJob.fromLens.createJob.error.timeRange', {
+ defaultMessage: 'Incompatible time range',
+ })
+ );
+ }
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ includeTimeRange = false;
+ start = undefined;
+ end = undefined;
+ }
+
+ return {
+ jobConfig,
+ datafeedConfig,
+ start,
+ end,
+ includeTimeRange,
+ };
+ }
+}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/route_resolver.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/route_resolver.ts
new file mode 100644
index 00000000000000..0f8462128d2cf5
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/route_resolver.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
+import type { DataPublicPluginStart, TimefilterContract } from '@kbn/data-plugin/public';
+import type { DashboardStart } from '@kbn/dashboard-plugin/public';
+import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
+import {
+ type CategorizationType,
+ QuickCategorizationJobCreator,
+ CATEGORIZATION_TYPE,
+} from './quick_create_job';
+import type { MlApiServices } from '../../../services/ml_api_service';
+
+import { getDefaultDatafeedQuery, getRisonValue } from '../utils/new_job_utils';
+
+interface Dependencies {
+ kibanaConfig: IUiSettingsClient;
+ timeFilter: TimefilterContract;
+ dashboardService: DashboardStart;
+ data: DataPublicPluginStart;
+ mlApiServices: MlApiServices;
+}
+export async function resolver(
+ deps: Dependencies,
+ categorizationTypeRisonString: string,
+ dataViewIdRisonString: string,
+ fieldRisonString: string,
+ partitionFieldRisonString: string | null,
+ stopOnWarnRisonString: string,
+ fromRisonString: string,
+ toRisonString: string,
+ queryRisonString: string
+) {
+ const { mlApiServices, timeFilter, kibanaConfig, dashboardService, data } = deps;
+
+ const query = getRisonValue(queryRisonString, getDefaultDatafeedQuery());
+ const from = getRisonValue(fromRisonString, '');
+ const to = getRisonValue(toRisonString, '');
+ const categorizationType = getRisonValue(
+ categorizationTypeRisonString,
+ CATEGORIZATION_TYPE.COUNT
+ );
+ const dataViewId = getRisonValue(dataViewIdRisonString, '');
+ const field = getRisonValue(fieldRisonString, '');
+ const partitionField =
+ partitionFieldRisonString === null ? '' : getRisonValue(partitionFieldRisonString, '');
+ const stopOnWarn = getRisonValue(stopOnWarnRisonString, false);
+
+ const jobCreator = new QuickCategorizationJobCreator(
+ kibanaConfig,
+ timeFilter,
+ dashboardService,
+ data,
+ mlApiServices
+ );
+ await jobCreator.createAndStashADJob(
+ categorizationType,
+ dataViewId,
+ field,
+ partitionField,
+ stopOnWarn,
+ from,
+ to,
+ query
+ );
+}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/utils.ts
new file mode 100644
index 00000000000000..4bc84a4df20dc7
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_pattern_analysis/utils.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import type { SharePluginStart } from '@kbn/share-plugin/public';
+import type { TimeRange } from '@kbn/es-query';
+import { ML_APP_LOCATOR } from '../../../../../common/constants/locator';
+import { ML_PAGES } from '../../../../locator';
+import type { CategorizationType } from './quick_create_job';
+
+export async function redirectToADJobWizards(
+ categorizationType: CategorizationType,
+ dataView: DataView,
+ field: DataViewField,
+ partitionField: DataViewField | null,
+ stopOnWarn: boolean,
+ query: QueryDslQueryContainer,
+ timeRange: TimeRange,
+ share: SharePluginStart
+) {
+ const locator = share.url.locators.get(ML_APP_LOCATOR)!;
+
+ const url = await locator.getUrl({
+ page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS,
+ pageState: {
+ categorizationType,
+ dataViewId: dataView.id,
+ field: field.name,
+ partitionField: partitionField?.name || null,
+ stopOnWarn,
+ from: timeRange.from,
+ to: timeRange.to,
+ query: JSON.stringify(query),
+ },
+ });
+
+ window.open(url, '_blank');
+}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts
index b13cde5e94ccdc..ba31ff5e4cd942 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts
@@ -55,6 +55,7 @@ async function getWizardUrlFromCloningJob(createdBy: string | undefined, dataVie
page = JOB_TYPE.POPULATION;
break;
case CREATED_BY_LABEL.CATEGORIZATION:
+ case CREATED_BY_LABEL.CATEGORIZATION_FROM_PATTERN_ANALYSIS:
page = JOB_TYPE.CATEGORIZATION;
break;
case CREATED_BY_LABEL.RARE:
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts
index 32fd17c9d1f109..eef866da2d287b 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts
@@ -7,6 +7,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { cloneDeep } from 'lodash';
+import rison from '@kbn/rison';
import {
Query,
fromKueryExpression,
@@ -162,3 +163,14 @@ export function checkCardinalitySuccess(data: any) {
return response;
}
+
+export function getRisonValue(
+ risonString: string,
+ defaultValue: T
+) {
+ try {
+ return rison.decode(risonString) as T;
+ } catch (error) {
+ return defaultValue;
+ }
+}
diff --git a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx
index 7c39528cf5b4a0..42e095edeccf54 100644
--- a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx
+++ b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx
@@ -130,18 +130,19 @@ export function useModelActions({
return useMemo(
() => [
{
- name: i18n.translate('xpack.ml.trainedModels.modelsList.viewTrainingDataActionLabel', {
+ name: i18n.translate('xpack.ml.trainedModels.modelsList.viewTrainingDataNameActionLabel', {
defaultMessage: 'View training data',
}),
description: i18n.translate(
'xpack.ml.trainedModels.modelsList.viewTrainingDataActionLabel',
{
- defaultMessage: 'View training data',
+ defaultMessage: 'Training data can be viewed when data frame analytics job exists.',
}
),
icon: 'visTable',
type: 'icon',
available: (item) => !!item.metadata?.analytics_config?.id,
+ enabled: (item) => item.origin_job_exists === true,
onClick: async (item) => {
if (item.metadata?.analytics_config === undefined) return;
@@ -164,7 +165,6 @@ export function useModelActions({
await navigateToUrl(url);
},
- isPrimary: true,
},
{
name: i18n.translate('xpack.ml.inference.modelsList.analyticsMapActionLabel', {
diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx
index 56486d1bbbd4f8..e9672729b6f4f6 100644
--- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx
+++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx
@@ -79,6 +79,7 @@ export type ModelItem = TrainedModelConfigResponse & {
type?: string[];
stats?: Stats & { deployment_stats: TrainedModelDeploymentStatsResponse[] };
pipelines?: ModelPipelines['pipelines'] | null;
+ origin_job_exists?: boolean;
deployment_ids: string[];
putModelConfig?: object;
state: ModelState;
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/from_pattern_analysis.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_pattern_analysis.tsx
new file mode 100644
index 00000000000000..1ac93184f201ed
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/from_pattern_analysis.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+import { Redirect } from 'react-router-dom';
+import { parse } from 'query-string';
+import { useMlKibana } from '../../../contexts/kibana';
+import { ML_PAGES } from '../../../../locator';
+import { createPath, MlRoute, PageLoader, PageProps } from '../../router';
+import { useRouteResolver } from '../../use_resolver';
+import { resolver } from '../../../jobs/new_job/job_from_pattern_analysis';
+
+export const fromPatternAnalysisRouteFactory = (): MlRoute => ({
+ path: createPath(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS),
+ render: (props, deps) => ,
+ breadcrumbs: [],
+});
+
+const PageWrapper: FC = ({ location }) => {
+ const {
+ categorizationType,
+ dataViewId,
+ field,
+ partitionField,
+ stopOnWarn,
+ from,
+ to,
+ query,
+ }: Record = parse(location.search, {
+ sort: false,
+ });
+ const {
+ services: {
+ data,
+ dashboard: dashboardService,
+ uiSettings: kibanaConfig,
+ mlServices: { mlApiServices },
+ },
+ } = useMlKibana();
+
+ const { context } = useRouteResolver('full', ['canCreateJob'], {
+ redirect: () =>
+ resolver(
+ {
+ mlApiServices,
+ timeFilter: data.query.timefilter.timefilter,
+ kibanaConfig,
+ dashboardService,
+ data,
+ },
+ categorizationType,
+ dataViewId,
+ field,
+ partitionField,
+ stopOnWarn,
+ from,
+ to,
+ query
+ ),
+ });
+
+ return (
+
+ {}
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index.ts b/x-pack/plugins/ml/public/application/routing/routes/new_job/index.ts
index 675b391d1e8263..d4876aba2444e1 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index.ts
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index.ts
@@ -12,3 +12,4 @@ export * from './wizard';
export * from './recognize';
export * from './from_lens';
export * from './from_map';
+export * from './from_pattern_analysis';
diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts
index f073c4afea8440..511a1a1457b02b 100644
--- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts
+++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts
@@ -14,15 +14,21 @@ import {
EVENT_RATE_FIELD_ID,
} from '@kbn/ml-anomaly-utils';
import { getGeoFields, filterCategoryFields } from '../../../../common/util/fields_utils';
-import { ml } from '../ml_api_service';
+import { ml, type MlApiServices } from '../ml_api_service';
import { processTextAndKeywordFields, NewJobCapabilitiesServiceBase } from './new_job_capabilities';
-class NewJobCapsService extends NewJobCapabilitiesServiceBase {
+export class NewJobCapsService extends NewJobCapabilitiesServiceBase {
private _catFields: Field[] = [];
private _dateFields: Field[] = [];
private _geoFields: Field[] = [];
private _includeEventRateField: boolean = true;
private _removeTextFields: boolean = true;
+ private _mlApiService: MlApiServices;
+
+ constructor(mlApiService: MlApiServices) {
+ super();
+ this._mlApiService = mlApiService;
+ }
public get catFields(): Field[] {
return this._catFields;
@@ -49,7 +55,10 @@ class NewJobCapsService extends NewJobCapabilitiesServiceBase {
this._includeEventRateField = includeEventRateField;
this._removeTextFields = removeTextFields;
- const resp = await ml.jobs.newJobCaps(dataView.getIndexPattern(), dataView.type === 'rollup');
+ const resp = await this._mlApiService.jobs.newJobCaps(
+ dataView.getIndexPattern(),
+ dataView.type === 'rollup'
+ );
const { fields: allFields, aggs } = createObjects(resp, dataView.getIndexPattern());
if (this._includeEventRateField === true) {
@@ -175,4 +184,4 @@ function addEventRateField(aggs: Aggregation[], fields: Field[]) {
fields.splice(0, 0, eventRateField);
}
-export const newJobCapsService = new NewJobCapsService();
+export const newJobCapsService = new NewJobCapsService(ml);
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/create_job.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/create_job.tsx
new file mode 100644
index 00000000000000..5e52b0f5cd2129
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/create_job.tsx
@@ -0,0 +1,273 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useCallback, useMemo, useState, useEffect } from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import {
+ EuiCheckableCard,
+ EuiTitle,
+ EuiSpacer,
+ EuiSwitch,
+ EuiHorizontalRule,
+ EuiComboBoxOptionOption,
+ EuiComboBox,
+ EuiFormRow,
+ EuiCallOut,
+} from '@elastic/eui';
+
+import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
+import type { TimeRange } from '@kbn/es-query';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { redirectToADJobWizards } from '../../../../application/jobs/new_job/job_from_pattern_analysis/utils';
+import { createFieldOptions } from '../../../../application/jobs/new_job/common/job_creator/util/general';
+import { NewJobCapsService } from '../../../../application/services/new_job_capabilities/new_job_capabilities_service';
+import {
+ type CategorizationType,
+ CATEGORIZATION_TYPE,
+ QuickCategorizationJobCreator,
+} from '../../../../application/jobs/new_job/job_from_pattern_analysis';
+import { useMlFromLensKibanaContext } from '../../common/context';
+import { JobDetails, type CreateADJobParams } from '../../common/job_details';
+
+interface Props {
+ dataView: DataView;
+ field: DataViewField;
+ query: QueryDslQueryContainer;
+ timeRange: TimeRange;
+}
+
+export const CreateJob: FC = ({ dataView, field, query, timeRange }) => {
+ const {
+ services: {
+ data,
+ share,
+ uiSettings,
+ mlServices: { mlApiServices },
+ dashboardService,
+ },
+ } = useMlFromLensKibanaContext();
+
+ const [categorizationType, setCategorizationType] = useState(
+ CATEGORIZATION_TYPE.COUNT
+ );
+ const [enablePerPartitionCategorization, setEnablePerPartitionCategorization] = useState(false);
+ const [stopOnWarn, setStopOnWarn] = useState(false);
+ const [categoryFieldOptions, setCategoryFieldsOptions] = useState([]);
+ const [selectedPartitionFieldOptions, setSelectedPartitionFieldOptions] = useState<
+ EuiComboBoxOptionOption[]
+ >([]);
+ const [formComplete, setFormComplete] = useState(undefined);
+
+ const toggleEnablePerPartitionCategorization = useCallback(
+ () => setEnablePerPartitionCategorization(!enablePerPartitionCategorization),
+ [enablePerPartitionCategorization]
+ );
+
+ const toggleStopOnWarn = useCallback(() => setStopOnWarn(!stopOnWarn), [stopOnWarn]);
+
+ useMemo(() => {
+ const newJobCapsService = new NewJobCapsService(mlApiServices);
+ newJobCapsService.initializeFromDataVIew(dataView).then(() => {
+ const options: EuiComboBoxOptionOption[] = [
+ ...createFieldOptions(newJobCapsService.categoryFields, []),
+ ].map((o) => ({
+ ...o,
+ }));
+ setCategoryFieldsOptions(options);
+ });
+ }, [dataView, mlApiServices]);
+
+ const quickJobCreator = useMemo(
+ () =>
+ new QuickCategorizationJobCreator(
+ uiSettings,
+ data.query.timefilter.timefilter,
+ dashboardService,
+ data,
+ mlApiServices
+ ),
+
+ [dashboardService, data, mlApiServices, uiSettings]
+ );
+
+ function createADJobInWizard() {
+ const partitionField = selectedPartitionFieldOptions.length
+ ? dataView.getFieldByName(selectedPartitionFieldOptions[0].label) ?? null
+ : null;
+ redirectToADJobWizards(
+ categorizationType,
+ dataView,
+ field,
+ partitionField,
+ stopOnWarn,
+ query,
+ timeRange,
+ share
+ );
+ }
+
+ useEffect(() => {
+ setSelectedPartitionFieldOptions([]);
+ setStopOnWarn(false);
+ }, [enablePerPartitionCategorization]);
+
+ useEffect(() => {
+ setFormComplete(
+ enablePerPartitionCategorization === false || selectedPartitionFieldOptions.length > 0
+ );
+ }, [enablePerPartitionCategorization, selectedPartitionFieldOptions]);
+
+ async function createADJob({ jobId, bucketSpan, startJob, runInRealTime }: CreateADJobParams) {
+ const partitionField = selectedPartitionFieldOptions.length
+ ? dataView.getFieldByName(selectedPartitionFieldOptions[0].label) ?? null
+ : null;
+ const result = await quickJobCreator.createAndSaveJob(
+ categorizationType,
+ jobId,
+ bucketSpan,
+ dataView,
+ field,
+ partitionField,
+ stopOnWarn,
+ query,
+ timeRange,
+ startJob,
+ runInRealTime
+ );
+ return result;
+ }
+ return (
+
+ <>
+
+
+
+
+
+
+
+
+ >
+ }
+ checked={categorizationType === CATEGORIZATION_TYPE.COUNT}
+ onChange={() => setCategorizationType(CATEGORIZATION_TYPE.COUNT)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
+ checked={categorizationType === CATEGORIZATION_TYPE.RARE}
+ onChange={() => setCategorizationType(CATEGORIZATION_TYPE.RARE)}
+ />
+
+
+ }
+ />
+
+ {enablePerPartitionCategorization ? (
+ <>
+
+
+
+ }
+ />
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+ }
+ />
+ >
+ ) : null}
+
+
+
+
+ >
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/flyout.tsx
new file mode 100644
index 00000000000000..48781b17761419
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/flyout.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiFlyoutBody,
+ EuiTitle,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+
+import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import type { TimeRange } from '@kbn/es-query';
+import { CreateJob } from './create_job';
+
+interface Props {
+ dataView: DataView;
+ field: DataViewField;
+ query: QueryDslQueryContainer;
+ timeRange: TimeRange;
+ onClose: () => void;
+}
+
+export const CreateCategorizationJobFlyout: FC = ({
+ onClose,
+ dataView,
+ field,
+ query,
+ timeRange,
+}) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/index.ts b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/index.ts
new file mode 100644
index 00000000000000..ab544dc3d55bd7
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/flyout/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { CreateCategorizationJobFlyout } from './flyout';
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/aiops/index.ts b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/index.ts
new file mode 100644
index 00000000000000..a156caa5ef57a8
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { showPatternAnalysisToADJobFlyout } from './show_flyout';
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/aiops/show_flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/show_flyout.tsx
new file mode 100644
index 00000000000000..c1fe261cbe9277
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/aiops/show_flyout.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+import type { CoreStart } from '@kbn/core/public';
+import type { SharePluginStart } from '@kbn/share-plugin/public';
+import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
+import type { LensPublicStart } from '@kbn/lens-plugin/public';
+import type { DashboardStart } from '@kbn/dashboard-plugin/public';
+import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import type { TimeRange } from '@kbn/es-query';
+import { createFlyout, type FlyoutComponentProps } from '../common/create_flyout';
+import { CreateCategorizationJobFlyout } from './flyout';
+
+export async function showPatternAnalysisToADJobFlyout(
+ dataView: DataView,
+ field: DataViewField,
+ query: QueryDslQueryContainer,
+ timeRange: TimeRange,
+ coreStart: CoreStart,
+ share: SharePluginStart,
+ data: DataPublicPluginStart,
+ dashboardService: DashboardStart,
+ lens?: LensPublicStart
+): Promise {
+ const Comp: FC = ({ onClose }) => (
+
+ );
+ return createFlyout(Comp, coreStart, share, data, dashboardService, lens);
+}
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/lens/context.ts b/x-pack/plugins/ml/public/embeddables/job_creation/common/context.ts
similarity index 100%
rename from x-pack/plugins/ml/public/embeddables/job_creation/lens/context.ts
rename to x-pack/plugins/ml/public/embeddables/job_creation/common/context.ts
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx
index 7fd5f9e86fca8c..b1bc9ec47ba941 100644
--- a/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/common/create_flyout.tsx
@@ -14,15 +14,16 @@ import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
-import type { MapEmbeddable } from '@kbn/maps-plugin/public';
-import type { Embeddable } from '@kbn/lens-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { getMlGlobalServices } from '../../../application/app';
+export interface FlyoutComponentProps {
+ onClose: () => void;
+}
+
export function createFlyout(
FlyoutComponent: React.FunctionComponent,
- embeddable: MapEmbeddable | Embeddable,
coreStart: CoreStart,
share: SharePluginStart,
data: DataPublicPluginStart,
@@ -57,7 +58,6 @@ export function createFlyout(
}}
>
{
onFlyoutClose();
resolve();
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx
index e2fff3bd286cf3..312d75a2a97b7c 100644
--- a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx
@@ -32,6 +32,7 @@ import type { Embeddable } from '@kbn/lens-plugin/public';
import type { MapEmbeddable } from '@kbn/maps-plugin/public';
import { extractErrorMessage } from '@kbn/ml-error-utils';
+import type { TimeRange } from '@kbn/es-query';
import { QuickLensJobCreator } from '../../../application/jobs/new_job/job_from_lens';
import type { LayerResult } from '../../../application/jobs/new_job/job_from_lens';
import type { CreateState } from '../../../application/jobs/new_job/job_from_dashboard';
@@ -40,12 +41,12 @@ import { basicJobValidation } from '../../../../common/util/job_utils';
import { JOB_ID_MAX_LENGTH } from '../../../../common/constants/validation';
import { invalidTimeIntervalMessage } from '../../../application/jobs/new_job/common/job_validator/util';
import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator';
-import { useMlFromLensKibanaContext } from '../lens/context';
+import { useMlFromLensKibanaContext } from './context';
export interface CreateADJobParams {
jobId: string;
bucketSpan: string;
- embeddable: MapEmbeddable | Embeddable;
+ embeddable: MapEmbeddable | Embeddable | undefined;
startJob: boolean;
runInRealTime: boolean;
}
@@ -56,8 +57,10 @@ interface Props {
createADJob: (args: CreateADJobParams) => Promise;
layer?: LayerResult;
layerIndex: number;
- embeddable: Embeddable | MapEmbeddable;
+ embeddable: Embeddable | MapEmbeddable | undefined;
+ timeRange: TimeRange | undefined;
incomingCreateError?: { text: string; errorText: string };
+ outerFormComplete?: boolean;
}
enum STATE {
@@ -75,7 +78,9 @@ export const JobDetails: FC = ({
layer,
layerIndex,
embeddable,
+ timeRange,
incomingCreateError,
+ outerFormComplete,
}) => {
const {
services: {
@@ -121,7 +126,6 @@ export const JobDetails: FC = ({
const viewResults = useCallback(
async (type: JOB_TYPE | null) => {
- const { timeRange } = embeddable.getInput();
const locator = share.url.locators.get(ML_APP_LOCATOR);
if (locator) {
const page = startJob
@@ -144,7 +148,7 @@ export const JobDetails: FC = ({
application.navigateToUrl(url);
}
},
- [jobId, embeddable, share, application, startJob]
+ [share, startJob, jobId, timeRange, application]
);
function setStartJobWrapper(start: boolean) {
@@ -313,7 +317,8 @@ export const JobDetails: FC = ({
state === STATE.VALIDATING ||
jobId === '' ||
jobIdValidationError !== '' ||
- bucketSpanValidationError !== ''
+ bucketSpanValidationError !== '' ||
+ outerFormComplete === false
}
onClick={createJob.bind(null, layerIndex)}
size="s"
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/flyout.tsx
index 06420c071c2209..dc0ab2edd4a1eb 100644
--- a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/flyout.tsx
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/flyout.tsx
@@ -23,7 +23,7 @@ import {
import { Layer } from './layer';
import type { LayerResult } from '../../../../application/jobs/new_job/job_from_lens';
import { VisualizationExtractor } from '../../../../application/jobs/new_job/job_from_lens';
-import { useMlFromLensKibanaContext } from '../context';
+import { useMlFromLensKibanaContext } from '../../common/context';
interface Props {
embeddable: Embeddable;
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/compatible_layer.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/compatible_layer.tsx
index 0d024bd2d77af3..d82ddbf94dd86a 100644
--- a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/compatible_layer.tsx
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/compatible_layer.tsx
@@ -17,7 +17,7 @@ import {
} from '../../../../../application/jobs/new_job/job_from_lens';
import type { LayerResult } from '../../../../../application/jobs/new_job/job_from_lens';
import { JOB_TYPE } from '../../../../../../common/constants/new_job';
-import { useMlFromLensKibanaContext } from '../../context';
+import { useMlFromLensKibanaContext } from '../../../common/context';
import { JobDetails, CreateADJobParams } from '../../../common/job_details';
interface Props {
@@ -79,6 +79,7 @@ export const CompatibleLayer: FC = ({ layer, layerIndex, embeddable }) =>
createADJob={createADJob}
createADJobInWizard={createADJobInWizard}
embeddable={embeddable}
+ timeRange={embeddable.getInput().timeRange}
layer={layer}
layerIndex={layerIndex}
>
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/lens/show_flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/lens/show_flyout.tsx
index 91e6f6f0018617..375588765bd189 100644
--- a/x-pack/plugins/ml/public/embeddables/job_creation/lens/show_flyout.tsx
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/lens/show_flyout.tsx
@@ -5,13 +5,14 @@
* 2.0.
*/
+import React, { FC } from 'react';
import type { Embeddable } from '@kbn/lens-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
-import { createFlyout } from '../common/create_flyout';
+import { createFlyout, type FlyoutComponentProps } from '../common/create_flyout';
import { LensLayerSelectionFlyout } from './lens_vis_layer_selection_flyout';
export async function showLensVisToADJobFlyout(
@@ -19,16 +20,11 @@ export async function showLensVisToADJobFlyout(
coreStart: CoreStart,
share: SharePluginStart,
data: DataPublicPluginStart,
- lens: LensPublicStart,
- dashboardService: DashboardStart
+ dashboardService: DashboardStart,
+ lens: LensPublicStart
): Promise {
- return createFlyout(
- LensLayerSelectionFlyout,
- embeddable,
- coreStart,
- share,
- data,
- dashboardService,
- lens
+ const Comp: FC = ({ onClose }) => (
+
);
+ return createFlyout(Comp, coreStart, share, data, dashboardService, lens);
}
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx
index d4075414e3a940..8f368dc0a82c03 100644
--- a/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/map/map_vis_layer_selection_flyout/layer/compatible_layer.tsx
@@ -25,7 +25,7 @@ import {
QuickGeoJobCreator,
redirectToGeoJobWizard,
} from '../../../../../application/jobs/new_job/job_from_map';
-import { useMlFromLensKibanaContext } from '../../../lens/context';
+import { useMlFromLensKibanaContext } from '../../../common/context';
import { JobDetails, CreateADJobParams } from '../../../common/job_details';
interface DropDownLabel {
@@ -147,6 +147,7 @@ export const CompatibleLayer: FC = ({ embeddable, layer, layerIndex }) =>
createADJob={createGeoJob}
createADJobInWizard={createGeoJobInWizard}
embeddable={embeddable}
+ timeRange={embeddable.getInput().timeRange}
incomingCreateError={createError}
>
<>
diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx
index 5380513f1dc97e..293ec69b30dbe5 100644
--- a/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx
+++ b/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import React, { FC } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
@@ -12,7 +13,7 @@ import type { MapEmbeddable } from '@kbn/maps-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { GeoJobFlyout } from './flyout';
-import { createFlyout } from '../common/create_flyout';
+import { createFlyout, type FlyoutComponentProps } from '../common/create_flyout';
export async function showMapVisToADJobFlyout(
embeddable: MapEmbeddable,
@@ -21,5 +22,8 @@ export async function showMapVisToADJobFlyout(
data: DataPublicPluginStart,
dashboardService: DashboardStart
): Promise {
- return createFlyout(GeoJobFlyout, embeddable, coreStart, share, data, dashboardService);
+ const Comp: FC = ({ onClose }) => (
+
+ );
+ return createFlyout(Comp, coreStart, share, data, dashboardService);
}
diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts
index e397778315a6a0..05fe312fd9a46b 100644
--- a/x-pack/plugins/ml/public/locator/ml_locator.ts
+++ b/x-pack/plugins/ml/public/locator/ml_locator.ts
@@ -85,6 +85,7 @@ export class MlLocatorDefinition implements LocatorDefinition {
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_LENS:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_MAP:
+ case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS:
case ML_PAGES.DATA_VISUALIZER:
case ML_PAGES.DATA_VISUALIZER_FILE:
case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER:
diff --git a/x-pack/plugins/ml/public/ui_actions/index.ts b/x-pack/plugins/ml/public/ui_actions/index.ts
index 4067547e089563..4e756d9d44d50f 100644
--- a/x-pack/plugins/ml/public/ui_actions/index.ts
+++ b/x-pack/plugins/ml/public/ui_actions/index.ts
@@ -8,9 +8,14 @@
import { CoreSetup } from '@kbn/core/public';
import { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
+import { CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER } from '@kbn/ml-ui-actions';
import { createEditSwimlanePanelAction } from './edit_swimlane_panel_action';
import { createOpenInExplorerAction } from './open_in_anomaly_explorer_action';
import { createVisToADJobAction } from './open_vis_in_ml_action';
+import {
+ createCategorizationADJobAction,
+ createCategorizationADJobTrigger,
+} from './open_create_categorization_job_action';
import { MlPluginStart, MlStartDependencies } from '../plugin';
import { createApplyInfluencerFiltersAction } from './apply_influencer_filters_action';
import {
@@ -45,6 +50,7 @@ export function registerMlUiActions(
const clearSelectionAction = createClearSelectionAction(core.getStartServices);
const editExplorerPanelAction = createEditAnomalyChartsPanelAction(core.getStartServices);
const visToAdJobAction = createVisToADJobAction(core.getStartServices);
+ const categorizationADJobAction = createCategorizationADJobAction(core.getStartServices);
// Register actions
uiActions.registerAction(editSwimlanePanelAction);
@@ -54,6 +60,7 @@ export function registerMlUiActions(
uiActions.registerAction(applyTimeRangeSelectionAction);
uiActions.registerAction(clearSelectionAction);
uiActions.registerAction(editExplorerPanelAction);
+ uiActions.registerAction(categorizationADJobAction);
// Assign triggers
uiActions.attachAction(CONTEXT_MENU_TRIGGER, editSwimlanePanelAction.id);
@@ -62,6 +69,7 @@ export function registerMlUiActions(
uiActions.registerTrigger(swimLaneSelectionTrigger);
uiActions.registerTrigger(entityFieldSelectionTrigger);
+ uiActions.registerTrigger(createCategorizationADJobTrigger);
uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, applyInfluencerFiltersAction);
uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, applyTimeRangeSelectionAction);
@@ -69,4 +77,8 @@ export function registerMlUiActions(
uiActions.addTriggerAction(SWIM_LANE_SELECTION_TRIGGER, clearSelectionAction);
uiActions.addTriggerAction(EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER, applyEntityFieldFilterAction);
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, visToAdJobAction);
+ uiActions.addTriggerAction(
+ CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
+ categorizationADJobAction
+ );
}
diff --git a/x-pack/plugins/ml/public/ui_actions/open_create_categorization_job_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_create_categorization_job_action.tsx
new file mode 100644
index 00000000000000..2855020a201dff
--- /dev/null
+++ b/x-pack/plugins/ml/public/ui_actions/open_create_categorization_job_action.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import type { Trigger, UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
+import {
+ CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION,
+ CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
+ type CreateCategorizationADJobContext,
+} from '@kbn/ml-ui-actions';
+import type { MlCoreSetup } from '../plugin';
+
+export const createCategorizationADJobTrigger: Trigger = {
+ id: CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_TRIGGER,
+ title: i18n.translate('xpack.ml.actions.createADJobFromPatternAnalysis', {
+ defaultMessage: 'Create categorization anomaly detection job',
+ }),
+ description: i18n.translate('xpack.ml.actions.createADJobFromPatternAnalysis', {
+ defaultMessage: 'Create categorization anomaly detection job',
+ }),
+};
+
+export function createCategorizationADJobAction(
+ getStartServices: MlCoreSetup['getStartServices']
+): UiActionsActionDefinition {
+ return {
+ id: 'create-ml-categorization-ad-job-action',
+ type: CREATE_PATTERN_ANALYSIS_TO_ML_AD_JOB_ACTION,
+ getIconType(context): string {
+ return 'machineLearningApp';
+ },
+ getDisplayName: () =>
+ i18n.translate('xpack.ml.actions.createADJobFromPatternAnalysis', {
+ defaultMessage: 'Create categorization anomaly detection job',
+ }),
+ async execute({ dataView, field, query, timeRange }: CreateCategorizationADJobContext) {
+ if (!dataView) {
+ throw new Error('Not possible to execute an action without the embeddable context');
+ }
+
+ try {
+ const [{ showPatternAnalysisToADJobFlyout }, [coreStart, { share, data, dashboard }]] =
+ await Promise.all([import('../embeddables/job_creation/aiops'), getStartServices()]);
+
+ await showPatternAnalysisToADJobFlyout(
+ dataView,
+ field,
+ query,
+ timeRange,
+ coreStart,
+ share,
+ data,
+ dashboard
+ );
+ } catch (e) {
+ return Promise.reject();
+ }
+ },
+ async isCompatible({ dataView, field }: CreateCategorizationADJobContext) {
+ return (
+ dataView.timeFieldName !== undefined &&
+ dataView.fields.find((f) => f.name === field.name) !== undefined
+ );
+ },
+ };
+}
diff --git a/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx
index fb0aa38e44d90a..f47df760ea9cdc 100644
--- a/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx
+++ b/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx
@@ -39,7 +39,7 @@ export function createVisToADJobAction(
if (lens === undefined) {
return;
}
- await showLensVisToADJobFlyout(embeddable, coreStart, share, data, lens, dashboard);
+ await showLensVisToADJobFlyout(embeddable, coreStart, share, data, dashboard, lens);
} else if (isMapEmbeddable(embeddable)) {
const [{ showMapVisToADJobFlyout }, [coreStart, { share, data, dashboard }]] =
await Promise.all([import('../embeddables/job_creation/map'), getStartServices()]);
diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts
index 8095411f911e7a..34cbaf755c1e1c 100644
--- a/x-pack/plugins/ml/server/routes/trained_models.ts
+++ b/x-pack/plugins/ml/server/routes/trained_models.ts
@@ -29,9 +29,9 @@ import {
createIngestPipelineSchema,
modelDownloadsQuery,
} from './schemas/inference_schema';
-import type {
+import {
PipelineDefinition,
- TrainedModelConfigResponse,
+ type TrainedModelConfigResponse,
} from '../../common/types/trained_models';
import { mlLog } from '../lib/log';
import { forceQuerySchema } from './schemas/anomaly_detectors_schema';
@@ -39,10 +39,9 @@ import { modelsProvider } from '../models/model_management';
export const DEFAULT_TRAINED_MODELS_PAGE_SIZE = 10000;
-export function filterForEnabledFeatureModels(
- models: TrainedModelConfigResponse[] | estypes.MlTrainedModelConfig[],
- enabledFeatures: MlFeatures
-) {
+export function filterForEnabledFeatureModels<
+ T extends TrainedModelConfigResponse | estypes.MlTrainedModelConfig
+>(models: T[], enabledFeatures: MlFeatures) {
let filteredModels = models;
if (enabledFeatures.nlp === false) {
filteredModels = filteredModels.filter((m) => m.model_type === 'tree_ensemble');
@@ -191,10 +190,38 @@ export function trainedModelsRoutes(
mlLog.debug(e);
}
- const body = filterForEnabledFeatureModels(result, getEnabledFeatures());
+ const filteredModels = filterForEnabledFeatureModels(result, getEnabledFeatures());
+
+ try {
+ const jobIds = filteredModels
+ .map((model) => {
+ const id = model.metadata?.analytics_config?.id;
+ if (id) {
+ return `${id}*`;
+ }
+ })
+ .filter((id) => id !== undefined);
+
+ if (jobIds.length) {
+ const { data_frame_analytics: jobs } = await mlClient.getDataFrameAnalytics({
+ id: jobIds.join(','),
+ allow_no_match: true,
+ });
+
+ filteredModels.forEach((model) => {
+ const dfaId = model?.metadata?.analytics_config?.id;
+ if (dfaId !== undefined) {
+ // if this is a dfa model, set origin_job_exists
+ model.origin_job_exists = jobs.find((job) => job.id === dfaId) !== undefined;
+ }
+ });
+ }
+ } catch (e) {
+ // Swallow error to prevent blocking trained models result
+ }
return response.ok({
- body,
+ body: filteredModels,
});
} catch (e) {
return response.customError(wrapError(e));
diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json
index cca832eee04237..846825569da661 100644
--- a/x-pack/plugins/ml/tsconfig.json
+++ b/x-pack/plugins/ml/tsconfig.json
@@ -96,6 +96,7 @@
"@kbn/ml-runtime-field-utils",
"@kbn/ml-date-utils",
"@kbn/ml-category-validator",
+ "@kbn/ml-ui-actions",
"@kbn/deeplinks-ml",
"@kbn/core-notifications-browser-mocks",
"@kbn/unified-field-list",
diff --git a/x-pack/plugins/osquery/public/form/timeout_field.tsx b/x-pack/plugins/osquery/public/form/timeout_field.tsx
index a9ad26d11e1736..5ef6627ba2c439 100644
--- a/x-pack/plugins/osquery/public/form/timeout_field.tsx
+++ b/x-pack/plugins/osquery/public/form/timeout_field.tsx
@@ -8,14 +8,7 @@ import React, { useCallback, useMemo } from 'react';
import deepEqual from 'fast-deep-equal';
import { useController } from 'react-hook-form';
import type { EuiFieldNumberProps } from '@elastic/eui';
-import {
- EuiFieldNumber,
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormRow,
- EuiIconTip,
- EuiText,
-} from '@elastic/eui';
+import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -81,16 +74,6 @@ const TimeoutFieldComponent = ({ euiFieldProps }: TimeoutFieldProps) => {
fullWidth
error={error?.message}
isInvalid={hasError}
- labelAppend={
-
-
-
-
-
- }
>
{
cy.get(`#${tabId}`).click();
};
-describe('Artifact tabs in Policy Details page', { tags: ['@ess', '@serverless'] }, () => {
+// FLAKY: https://github.com/elastic/kibana/issues/171644
+describe.skip('Artifact tabs in Policy Details page', { tags: ['@ess', '@serverless'] }, () => {
let endpointData: ReturnTypeFromChainable | undefined;
before(() => {
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts
index e7f32820c00d7d..531b118f3fcd3b 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifacts_mocked_data.cy.ts
@@ -31,7 +31,8 @@ const loginWithoutAccess = (url: string) => {
loadPage(url);
};
-describe('Artifacts pages', { tags: ['@ess', '@serverless'] }, () => {
+// FLAKY: https://github.com/elastic/kibana/issues/171168
+describe.skip('Artifacts pages', { tags: ['@ess', '@serverless'] }, () => {
let endpointData: ReturnTypeFromChainable | undefined;
before(() => {
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/reponse_actions_history.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/reponse_actions_history.cy.ts
index 4efd03c01d05bc..0af8ee43f29883 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/reponse_actions_history.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/reponse_actions_history.cy.ts
@@ -10,7 +10,8 @@ import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
-describe('Response actions history page', { tags: ['@ess', '@serverless'] }, () => {
+// FLAKY: https://github.com/elastic/kibana/issues/171641
+describe.skip('Response actions history page', { tags: ['@ess', '@serverless'] }, () => {
let endpointData: ReturnTypeFromChainable;
// let actionData: ReturnTypeFromChainable;
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts
index 9cd298535df26d..a28d710091eb47 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts
@@ -15,7 +15,8 @@ import {
visitEndpointList,
} from '../../screens';
-describe(
+// FLAKY: https://github.com/elastic/kibana/issues/171643
+describe.skip(
'When on the Endpoint List in Security Essentials PLI',
{
tags: ['@serverless'],
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts
index a197574035b79f..722b31aca0391d 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts
@@ -30,7 +30,8 @@ import {
openConsoleHelpPanel,
} from '../../../screens';
-describe(
+// FLAKY: https://github.com/elastic/kibana/issues/170052
+describe.skip(
'User Roles for Security Complete PLI with Endpoint Complete addon',
{
tags: ['@serverless'],
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts
index 4e0ae2080a97b7..03ff413405b53f 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts
@@ -23,7 +23,8 @@ import {
ensurePolicyDetailsPageAuthzAccess,
} from '../../../screens';
-describe(
+// FLAKY: https://github.com/elastic/kibana/issues/170985
+describe.skip(
'Roles for Security Essential PLI with Endpoint Essentials addon',
{
tags: ['@serverless'],
diff --git a/x-pack/plugins/security_solution/scripts/junit_transformer/lib.ts b/x-pack/plugins/security_solution/scripts/junit_transformer/lib.ts
index d9033c75d485b2..725baa689cd207 100644
--- a/x-pack/plugins/security_solution/scripts/junit_transformer/lib.ts
+++ b/x-pack/plugins/security_solution/scripts/junit_transformer/lib.ts
@@ -16,6 +16,7 @@ import * as t from 'io-ts';
import { isLeft } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import globby from 'globby';
+import del from 'del';
/**
* Updates the `name` and `classname` attributes of each testcase.
@@ -195,6 +196,9 @@ ${boilerplate}
if ('error' in maybeValidationResult) {
logError(maybeValidationResult.error);
+ // Sending broken XML to Failed Test Reporter will cause a job to fail
+ await del(path);
+ log.warning(`${path} file was deleted.`);
// If there is an error, continue trying to process other files.
continue;
}
@@ -203,9 +207,11 @@ ${boilerplate}
const { processed, hadTestCases } = isReportAlreadyProcessed(reportJson);
if (hadTestCases === false) {
- log.warning(`${path} had no test cases. Skipping it.
+ log.warning(`${path} had no test cases.
${boilerplate}
`);
+ await del(path);
+ log.warning(`${path} file was deleted.`);
// If there is an error, continue trying to process other files.
continue;
}
@@ -222,6 +228,9 @@ ${boilerplate}
if ('error' in maybeSpecFilePath) {
logError(maybeSpecFilePath.error);
+ // Sending broken XML to Failed Test Reporter will cause a job to fail
+ await del(path);
+ log.warning(`${path} file was deleted.`);
// If there is an error, continue trying to process other files.
continue;
}
diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/dotnet.ts b/x-pack/plugins/serverless_search/public/application/components/languages/dotnet.ts
index 59b7e9e5721c72..4f388d2227203d 100644
--- a/x-pack/plugins/serverless_search/public/application/components/languages/dotnet.ts
+++ b/x-pack/plugins/serverless_search/public/application/components/languages/dotnet.ts
@@ -23,7 +23,7 @@ export const dotnetDefinition: LanguageDefinition = {
installClient: 'dotnet add package Elastic.Clients.Elasticsearch.Serverless',
configureClient: ({ apiKey, cloudId }) => `using System;
using Elastic.Clients.Elasticsearch.Serverless;
-using Elastic.Clients.Elasticsearch.QueryDsl;
+using Elastic.Clients.Elasticsearch.Serverless.QueryDsl;
var client = new ElasticsearchClient("${cloudId}", new ApiKey("${apiKey}"));`,
testConnection: `var info = await client.InfoAsync();`,
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts
index 061e8936bf241d..3c31e714ae10bc 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts
@@ -17,7 +17,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const endpointTestResources = getService('endpointTestResources');
- describe('Endpoint permissions:', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/171649
+ // FLAKY: https://github.com/elastic/kibana/issues/171650
+ describe.skip('Endpoint permissions:', function () {
targetTags(this, ['@ess']);
let indexedData: IndexedHostsAndAlertsResponse;
diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts
index b48c64b5e9dd4a..a2b1feaabd7cd3 100644
--- a/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts
+++ b/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts
@@ -28,7 +28,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const endpointTestResources = getService('endpointTestResources');
const retry = getService('retry');
- describe('When on the Endpoint Policy Details Page', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/171653
+ // FLAKY: https://github.com/elastic/kibana/issues/171654
+ describe.skip('When on the Endpoint Policy Details Page', function () {
targetTags(this, ['@ess', '@serverless']);
let indexedData: IndexedHostsAndAlertsResponse;
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts
index 7434f46ca35be6..79f964f8c42713 100644
--- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts
@@ -40,7 +40,12 @@ export default function ({ getService }: FtrProviderContext) {
body: Record | undefined;
}
- describe('When attempting to call an endpoint api', function () {
+ // Flaky:
+ // https://github.com/elastic/kibana/issues/171655
+ // https://github.com/elastic/kibana/issues/171656
+ // https://github.com/elastic/kibana/issues/171647
+ // https://github.com/elastic/kibana/issues/171648
+ describe.skip('When attempting to call an endpoint api', function () {
targetTags(this, ['@ess', '@serverless']);
let indexedData: IndexedHostsAndAlertsResponse;
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts
index f904539dae231a..8d297f82befee5 100644
--- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts
@@ -16,7 +16,9 @@ export default function ({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const endpointTestResources = getService('endpointTestResources');
- describe('Endpoint `execute` response action', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/171667
+ // FLAKY: https://github.com/elastic/kibana/issues/171666
+ describe.skip('Endpoint `execute` response action', function () {
targetTags(this, ['@ess', '@serverless']);
let indexedData: IndexedHostsAndAlertsResponse;
diff --git a/yarn.lock b/yarn.lock
index 8016568cd10f5a..a3a29ac19c0701 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5069,6 +5069,10 @@
version "0.0.0"
uid ""
+"@kbn/ml-ui-actions@link:x-pack/packages/ml/ui_actions":
+ version "0.0.0"
+ uid ""
+
"@kbn/ml-url-state@link:x-pack/packages/ml/url_state":
version "0.0.0"
uid ""
@@ -8386,10 +8390,10 @@
"@tanstack/query-core" "4.29.11"
use-sync-external-store "^1.2.0"
-"@testim/chrome-version@^1.1.3":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.3.tgz#fbb68696899d7b8c1b9b891eded9c04fe2cd5529"
- integrity sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A==
+"@testim/chrome-version@^1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz#86e04e677cd6c05fa230dd15ac223fa72d1d7090"
+ integrity sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==
"@testing-library/dom@^8.0.0":
version "8.19.0"
@@ -11574,7 +11578,7 @@ axios@^0.26.0:
dependencies:
follow-redirects "^1.14.8"
-axios@^1.3.4, axios@^1.4.0, axios@^1.6.0:
+axios@^1.3.4, axios@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
@@ -12846,18 +12850,18 @@ chrome-trace-event@^1.0.2:
dependencies:
tslib "^1.9.0"
-chromedriver@^119.0.0:
- version "119.0.0"
- resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-119.0.0.tgz#f250c442e72266f3e28d2b1eebc6a671a8629340"
- integrity sha512-3TmabGT7xg57/Jbsg6B/Kqk3HaSbCP1ZHkR5zNft5vT/IWKjZCAGTH9waMI+i5KHSEiMH0zOw/WF98l+1Npkpw==
+chromedriver@^119.0.1:
+ version "119.0.1"
+ resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-119.0.1.tgz#064f3650790ccea055e9bfd95c600f5ea60295e9"
+ integrity sha512-lpCFFLaXPpvElTaUOWKdP74pFb/sJhWtWqMjn7Ju1YriWn8dT5JBk84BGXMPvZQs70WfCYWecxdMmwfIu1Mupg==
dependencies:
- "@testim/chrome-version" "^1.1.3"
- axios "^1.4.0"
- compare-versions "^6.0.0"
+ "@testim/chrome-version" "^1.1.4"
+ axios "^1.6.0"
+ compare-versions "^6.1.0"
extract-zip "^2.0.1"
https-proxy-agent "^5.0.1"
proxy-from-env "^1.1.0"
- tcp-port-used "^1.0.1"
+ tcp-port-used "^1.0.2"
chromium-bidi@0.4.28:
version "0.4.28"
@@ -13266,7 +13270,7 @@ compare-versions@3.5.1:
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393"
integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg==
-compare-versions@^6.0.0:
+compare-versions@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a"
integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==
@@ -14367,13 +14371,6 @@ debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, de
dependencies:
ms "2.1.2"
-debug@4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87"
- integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==
- dependencies:
- ms "^2.1.1"
-
debug@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@@ -14381,6 +14378,13 @@ debug@4.1.1:
dependencies:
ms "^2.1.1"
+debug@4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+ integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+ dependencies:
+ ms "2.1.2"
+
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@@ -18854,10 +18858,10 @@ io-ts@^2.0.5:
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.0.5.tgz#e6e3db9df8b047f9cbd6b69e7d2ad3e6437a0b13"
integrity sha512-pL7uUptryanI5Glv+GUv7xh+aLBjxGEDmLwmEYNSx0yOD3djK0Nw5Bt0N6BAkv9LadOUU7QKpRsLcqnTh3UlLA==
-ip-regex@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
- integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+ip-regex@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
+ integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
ip@^1.1.8:
version "1.1.8"
@@ -19417,7 +19421,7 @@ is-url-superb@^4.0.0:
resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-4.0.0.tgz#b54d1d2499bb16792748ac967aa3ecb41a33a8c2"
integrity sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==
-is-url@^1.2.2, is-url@^1.2.4:
+is-url@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
@@ -19491,14 +19495,14 @@ is-yarn-global@^0.3.0:
resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
-is2@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.1.tgz#8ac355644840921ce435d94f05d3a94634d3481a"
- integrity sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==
+is2@^2.0.6:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.9.tgz#ff63b441f90de343fa8fac2125ee170da8e8240d"
+ integrity sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==
dependencies:
deep-is "^0.1.3"
- ip-regex "^2.1.0"
- is-url "^1.2.2"
+ ip-regex "^4.1.0"
+ is-url "^1.2.4"
isarray@0.0.1:
version "0.0.1"
@@ -28592,13 +28596,13 @@ tcomb@^3.0.0, tcomb@^3.2.17:
resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-3.2.29.tgz#32404fe9456d90c2cf4798682d37439f1ccc386c"
integrity sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==
-tcp-port-used@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70"
- integrity sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==
+tcp-port-used@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz#9652b7436eb1f4cfae111c79b558a25769f6faea"
+ integrity sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==
dependencies:
- debug "4.1.0"
- is2 "2.0.1"
+ debug "4.3.1"
+ is2 "^2.0.6"
teex@^1.0.1:
version "1.0.1"