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"