From 4697b7efe19ae2599c4edf416fcb7312dbf2a2f2 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 19 Nov 2020 15:34:21 +0100 Subject: [PATCH 01/16] [7.x] Make expectSnapshot available (#82932) (#83770) Co-authored-by: spalger --- .../src/functional_test_runner/cli.ts | 7 +- .../fake_mocha_types.d.ts | 7 + .../lib/config/schema.ts | 2 +- .../lib/mocha/load_test_files.js | 13 +- .../lib/mocha/setup_mocha.js | 1 + .../snapshots/decorate_snapshot_ui.test.ts | 133 ++++++++++++++++++ .../lib/snapshots/decorate_snapshot_ui.ts | 99 +++++++------ .../run_tests/__snapshots__/args.test.js.snap | 23 +++ .../run_tests/__snapshots__/cli.test.js.snap | 2 + .../functional_tests/cli/run_tests/args.js | 6 + .../cli/run_tests/args.test.js | 5 + .../cli/run_tests/cli.test.js | 16 +++ .../__snapshots__/cli.test.js.snap | 9 +- .../cli/start_servers/cli.test.js | 9 ++ .../src/functional_tests/lib/run_ftr.js | 3 +- packages/kbn-test/tsconfig.json | 3 + .../kbn-test/types/ftr_globals/mocha.d.ts | 18 +-- .../kbn-test/types/ftr_globals/snapshots.d.ts | 25 ++++ test/tsconfig.json | 2 +- .../api_integration/apis/uptime/rest/index.ts | 3 - .../uptime/rest/monitor_states_real_data.ts | 1 - .../basic/tests/correlations/ranges.ts | 1 - .../tests/correlations/slow_durations.ts | 1 - .../apm_api_integration/basic/tests/index.ts | 3 - .../tests/metrics_charts/metrics_charts.ts | 1 - .../observability_overview.ts | 1 - .../basic/tests/service_maps/service_maps.ts | 1 - .../tests/service_overview/error_groups.ts | 1 - .../basic/tests/services/top_services.ts | 1 - .../basic/tests/services/transaction_types.ts | 1 - .../tests/settings/agent_configuration.ts | 1 - .../settings/anomaly_detection/read_user.ts | 1 - .../settings/anomaly_detection/write_user.ts | 1 - .../basic/tests/settings/custom_link.ts | 1 - .../basic/tests/traces/top_traces.ts | 1 - .../tests/transaction_groups/breakdown.ts | 1 - .../tests/transaction_groups/distribution.ts | 1 - .../tests/transaction_groups/error_rate.ts | 1 - .../top_transaction_groups.ts | 1 - .../transaction_groups/transaction_charts.ts | 1 - .../trial/tests/csm/csm_services.ts | 1 - .../trial/tests/csm/has_rum_data.ts | 1 - .../trial/tests/csm/js_errors.ts | 1 - .../trial/tests/csm/long_task_metrics.ts | 1 - .../trial/tests/csm/page_views.ts | 1 - .../trial/tests/csm/url_search.ts | 1 - .../trial/tests/csm/web_core_vitals.ts | 1 - .../apm_api_integration/trial/tests/index.ts | 3 - .../trial/tests/service_maps/service_maps.ts | 1 - .../trial/tests/services/top_services.ts | 1 - .../services/transaction_groups_charts.ts | 1 - x-pack/test/mocha_decorations.d.ts | 17 --- x-pack/test/tsconfig.json | 2 +- 53 files changed, 320 insertions(+), 120 deletions(-) create mode 100644 packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts rename x-pack/test/apm_api_integration/common/match_snapshot.ts => packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts (66%) rename test/mocha_decorations.d.ts => packages/kbn-test/types/ftr_globals/mocha.d.ts (73%) create mode 100644 packages/kbn-test/types/ftr_globals/snapshots.d.ts delete mode 100644 x-pack/test/mocha_decorations.d.ts diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 8a2075b2e7bda..4e8e01fcc1a6d 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -60,7 +60,8 @@ export function runFtrCli() { include: toArray(flags['include-tag'] as string | string[]), exclude: toArray(flags['exclude-tag'] as string | string[]), }, - updateBaselines: flags.updateBaselines, + updateBaselines: flags.updateBaselines || flags.u, + updateSnapshots: flags.updateSnapshots || flags.u, } ); @@ -118,7 +119,7 @@ export function runFtrCli() { 'exclude-tag', 'kibana-install-dir', ], - boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], + boolean: ['bail', 'invert', 'test-stats', 'updateBaselines', 'updateSnapshots', 'u'], default: { config: 'test/functional/config.js', }, @@ -133,6 +134,8 @@ export function runFtrCli() { --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags --test-stats print the number of tests (included and excluded) to STDERR --updateBaselines replace baseline screenshots with whatever is generated from the test + --updateSnapshots replace inline and file snapshots with whatever is generated from the test + -u replace both baseline screenshots and snapshots --kibana-install-dir directory where the Kibana install being tested resides `, }, diff --git a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts index 12390a95a4961..35b4b85e4d22a 100644 --- a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts +++ b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts @@ -28,10 +28,17 @@ import EventEmitter from 'events'; export interface Suite { suites: Suite[]; tests: Test[]; + title: string; + file?: string; + parent?: Suite; } export interface Test { fullTitle(): string; + title: string; + file?: string; + parent?: Suite; + isPassed: () => boolean; } export interface Runner extends EventEmitter { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 6ed114d62e244..6f1519c079bee 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -138,7 +138,7 @@ export const schema = Joi.object() .default(), updateBaselines: Joi.boolean().default(false), - + updateSnapshots: Joi.boolean().default(false), browser: Joi.object() .keys({ type: Joi.string().valid('chrome', 'firefox', 'msedge').default('chrome'), diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js index 5c23be6361866..0f5f3df6bd413 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js @@ -21,6 +21,7 @@ import { isAbsolute } from 'path'; import { loadTracer } from '../load_tracer'; import { decorateMochaUi } from './decorate_mocha_ui'; +import { decorateSnapshotUi } from '../snapshots/decorate_snapshot_ui'; /** * Load an array of test files into a mocha instance @@ -31,7 +32,17 @@ import { decorateMochaUi } from './decorate_mocha_ui'; * @param {String} path * @return {undefined} - mutates mocha, no return value */ -export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, updateBaselines }) => { +export const loadTestFiles = ({ + mocha, + log, + lifecycle, + providers, + paths, + updateBaselines, + updateSnapshots, +}) => { + decorateSnapshotUi(lifecycle, updateSnapshots); + const innerLoadTestFile = (path) => { if (typeof path !== 'string' || !isAbsolute(path)) { throw new TypeError('loadTestFile() only accepts absolute paths'); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js index 39eb69a151918..66b93ec001ac9 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js @@ -53,6 +53,7 @@ export async function setupMocha(lifecycle, log, config, providers) { providers, paths: config.get('testFiles'), updateBaselines: config.get('updateBaselines'), + updateSnapshots: config.get('updateSnapshots'), }); // Each suite has a tag that is the path relative to the root of the repo diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts new file mode 100644 index 0000000000000..abfbd8acea783 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Test } from '../../fake_mocha_types'; +import { Lifecycle } from '../lifecycle'; +import { decorateSnapshotUi, expectSnapshot } from './decorate_snapshot_ui'; +import path from 'path'; +import fs from 'fs'; + +describe('decorateSnapshotUi', () => { + describe('when running a test', () => { + let lifecycle: Lifecycle; + beforeEach(() => { + lifecycle = new Lifecycle(); + decorateSnapshotUi(lifecycle, false); + }); + + it('passes when the snapshot matches the actual value', async () => { + const test: Test = { + title: 'Test', + file: 'foo.ts', + parent: { + file: 'foo.ts', + tests: [], + suites: [], + }, + } as any; + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('foo').toMatchInline(`"foo"`); + }).not.toThrow(); + }); + + it('throws when the snapshot does not match the actual value', async () => { + const test: Test = { + title: 'Test', + file: 'foo.ts', + parent: { + file: 'foo.ts', + tests: [], + suites: [], + }, + } as any; + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('foo').toMatchInline(`"bar"`); + }).toThrow(); + }); + + it('writes a snapshot to an external file if it does not exist', async () => { + const test: Test = { + title: 'Test', + file: __filename, + isPassed: () => true, + } as any; + + // @ts-expect-error + test.parent = { + file: __filename, + tests: [test], + suites: [], + }; + + await lifecycle.beforeEachTest.trigger(test); + + const snapshotFile = path.resolve( + __dirname, + '__snapshots__', + 'decorate_snapshot_ui.test.snap' + ); + + expect(fs.existsSync(snapshotFile)).toBe(false); + + expect(() => { + expectSnapshot('foo').toMatch(); + }).not.toThrow(); + + await lifecycle.afterTestSuite.trigger(test.parent); + + expect(fs.existsSync(snapshotFile)).toBe(true); + + fs.unlinkSync(snapshotFile); + + fs.rmdirSync(path.resolve(__dirname, '__snapshots__')); + }); + }); + + describe('when updating snapshots', () => { + let lifecycle: Lifecycle; + beforeEach(() => { + lifecycle = new Lifecycle(); + decorateSnapshotUi(lifecycle, true); + }); + + it("doesn't throw if the value does not match", async () => { + const test: Test = { + title: 'Test', + file: 'foo.ts', + parent: { + file: 'foo.ts', + tests: [], + suites: [], + }, + } as any; + + await lifecycle.beforeEachTest.trigger(test); + + expect(() => { + expectSnapshot('bar').toMatchInline(`"foo"`); + }).not.toThrow(); + }); + }); +}); diff --git a/x-pack/test/apm_api_integration/common/match_snapshot.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts similarity index 66% rename from x-pack/test/apm_api_integration/common/match_snapshot.ts rename to packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts index 567a1ced360f8..45550b55e73c7 100644 --- a/x-pack/test/apm_api_integration/common/match_snapshot.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import { @@ -14,8 +27,9 @@ import path from 'path'; import expect from '@kbn/expect'; import prettier from 'prettier'; import babelTraverse from '@babel/traverse'; -import { Suite, Test } from 'mocha'; -import { flatten } from 'lodash'; +import { flatten, once } from 'lodash'; +import { Lifecycle } from '../lifecycle'; +import { Test, Suite } from '../../fake_mocha_types'; type ISnapshotState = InstanceType; @@ -59,12 +73,38 @@ function getSnapshotMeta(currentTest: Test) { }; } -export function registerMochaHooksForSnapshots() { +const modifyStackTracePrepareOnce = once(() => { + const originalPrepareStackTrace = Error.prepareStackTrace; + + // jest-snapshot uses a stack trace to determine which file/line/column + // an inline snapshot should be written to. We filter out match_snapshot + // from the stack trace to prevent it from wanting to write to this file. + + Error.prepareStackTrace = (error, structuredStackTrace) => { + let filteredStrackTrace: NodeJS.CallSite[] = structuredStackTrace; + if (registered) { + filteredStrackTrace = filteredStrackTrace.filter((callSite) => { + // check for both compiled and uncompiled files + return !callSite.getFileName()?.match(/decorate_snapshot_ui\.(js|ts)/); + }); + } + + if (originalPrepareStackTrace) { + return originalPrepareStackTrace(error, filteredStrackTrace); + } + }; +}); + +export function decorateSnapshotUi(lifecycle: Lifecycle, updateSnapshots: boolean) { let snapshotStatesByFilePath: Record< string, { snapshotState: ISnapshotState; testsInFile: Test[] } > = {}; + registered = true; + + modifyStackTracePrepareOnce(); + addSerializer({ serialize: (num: number) => { return String(parseFloat(num.toPrecision(15))); @@ -74,15 +114,14 @@ export function registerMochaHooksForSnapshots() { }, }); - registered = true; - - beforeEach(function () { - const currentTest = this.currentTest!; + // @ts-expect-error + global.expectSnapshot = expectSnapshot; + lifecycle.beforeEachTest.add((currentTest: Test) => { const { file, snapshotTitle } = getSnapshotMeta(currentTest); if (!snapshotStatesByFilePath[file]) { - snapshotStatesByFilePath[file] = getSnapshotState(file, currentTest); + snapshotStatesByFilePath[file] = getSnapshotState(file, currentTest, updateSnapshots); } testContext = { @@ -95,17 +134,14 @@ export function registerMochaHooksForSnapshots() { }; }); - afterEach(function () { - testContext = null; - }); - - after(function () { - // save snapshot after tests complete + lifecycle.afterTestSuite.add(function (testSuite) { + // save snapshot & check unused after top-level test suite completes + if (testSuite.parent?.parent) { + return; + } const unused: string[] = []; - const isUpdatingSnapshots = process.env.UPDATE_SNAPSHOTS; - Object.keys(snapshotStatesByFilePath).forEach((file) => { const { snapshotState, testsInFile } = snapshotStatesByFilePath[file]; @@ -118,7 +154,7 @@ export function registerMochaHooksForSnapshots() { } }); - if (!isUpdatingSnapshots) { + if (!updateSnapshots) { unused.push(...snapshotState.getUncheckedKeys()); } else { snapshotState.removeUncheckedKeys(); @@ -131,36 +167,19 @@ export function registerMochaHooksForSnapshots() { throw new Error( `${unused.length} obsolete snapshot(s) found:\n${unused.join( '\n\t' - )}.\n\nRun tests again with \`UPDATE_SNAPSHOTS=1\` to remove them.` + )}.\n\nRun tests again with \`--updateSnapshots\` to remove them.` ); } snapshotStatesByFilePath = {}; - - registered = false; }); } -const originalPrepareStackTrace = Error.prepareStackTrace; - -// jest-snapshot uses a stack trace to determine which file/line/column -// an inline snapshot should be written to. We filter out match_snapshot -// from the stack trace to prevent it from wanting to write to this file. - -Error.prepareStackTrace = (error, structuredStackTrace) => { - const filteredStrackTrace = structuredStackTrace.filter((callSite) => { - return !callSite.getFileName()?.endsWith('match_snapshot.ts'); - }); - if (originalPrepareStackTrace) { - return originalPrepareStackTrace(error, filteredStrackTrace); - } -}; - function recursivelyGetTestsFromSuite(suite: Suite): Test[] { return suite.tests.concat(flatten(suite.suites.map((s) => recursivelyGetTestsFromSuite(s)))); } -function getSnapshotState(file: string, test: Test) { +function getSnapshotState(file: string, test: Test, updateSnapshots: boolean) { const dirname = path.dirname(file); const filename = path.basename(file); @@ -177,7 +196,7 @@ function getSnapshotState(file: string, test: Test) { const snapshotState = new SnapshotState( path.join(dirname + `/__snapshots__/` + filename.replace(path.extname(filename), '.snap')), { - updateSnapshot: process.env.UPDATE_SNAPSHOTS ? 'all' : 'new', + updateSnapshot: updateSnapshots ? 'all' : 'new', getPrettier: () => prettier, getBabelTraverse: () => babelTraverse, } diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap index 434c374d5d23d..ad2f82de87b82 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --updateSnapshots Replace inline and file snapshots with whatever is generated from the test. + --u Replace both baseline screenshots and snapshots --include Files that must included to be run, can be included multiple times. --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. @@ -48,6 +50,27 @@ Object { } `; +exports[`process options for run tests CLI accepts boolean value for updateSnapshots 1`] = ` +Object { + "assertNoneExcluded": false, + "configs": Array [ + /foo, + ], + "createLogger": [Function], + "esFrom": "snapshot", + "extraKbnOpts": undefined, + "suiteFiles": Object { + "exclude": Array [], + "include": Array [], + }, + "suiteTags": Object { + "exclude": Array [], + "include": Array [], + }, + "updateSnapshots": true, +} +`; + exports[`process options for run tests CLI accepts debug option 1`] = ` Object { "assertNoneExcluded": false, diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap index 6ede71a6c3940..02d11b0033d57 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/cli.test.js.snap @@ -16,6 +16,8 @@ Options: --bail Stop the test run at the first failure. --grep Pattern to select which tests to run. --updateBaselines Replace baseline screenshots with whatever is generated from the test. + --updateSnapshots Replace inline and file snapshots with whatever is generated from the test. + --u Replace both baseline screenshots and snapshots --include Files that must included to be run, can be included multiple times. --exclude Files that must NOT be included to be run, can be included multiple times. --include-tag Tags that suites must include to be run, can be included multiple times. diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index 94d510915d8e5..5af0fe7d7b61c 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -46,6 +46,12 @@ const options = { updateBaselines: { desc: 'Replace baseline screenshots with whatever is generated from the test.', }, + updateSnapshots: { + desc: 'Replace inline and file snapshots with whatever is generated from the test.', + }, + u: { + desc: 'Replace both baseline screenshots and snapshots', + }, include: { arg: '', desc: 'Files that must included to be run, can be included multiple times.', diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js index 35e4cef5b3a66..34a2d19c22a3f 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js @@ -76,6 +76,11 @@ describe('process options for run tests CLI', () => { expect(options).toMatchSnapshot(); }); + it('accepts boolean value for updateSnapshots', () => { + const options = processOptions({ updateSnapshots: true }, ['foo']); + expect(options).toMatchSnapshot(); + }); + it('accepts source value for esFrom', () => { const options = processOptions({ esFrom: 'source' }, ['foo']); expect(options).toMatchSnapshot(); diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js index 97b74a3b2b541..f8820d98eb868 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js @@ -125,6 +125,22 @@ describe('run tests CLI', () => { expect(exitMock).not.toHaveBeenCalledWith(); }); + it('accepts boolean value for updateSnapshots', async () => { + global.process.argv.push('--updateSnapshots'); + + await runTestsCli(['foo']); + + expect(exitMock).not.toHaveBeenCalledWith(); + }); + + it('accepts boolean value for -u', async () => { + global.process.argv.push('-u'); + + await runTestsCli(['foo']); + + expect(exitMock).not.toHaveBeenCalledWith(); + }); + it('accepts source value for esFrom', async () => { global.process.argv.push('--esFrom', 'source'); diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/cli.test.js.snap b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/cli.test.js.snap index b54bf5dc84dd1..ba085b0868216 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/cli.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/cli.test.js.snap @@ -7,6 +7,13 @@ exports[`start servers CLI options accepts boolean value for updateBaselines 1`] " `; +exports[`start servers CLI options accepts boolean value for updateSnapshots 1`] = ` +" +functional_tests_server: invalid option [updateSnapshots] + ...stack trace... +" +`; + exports[`start servers CLI options rejects bail 1`] = ` " functional_tests_server: invalid option [bail] @@ -40,4 +47,4 @@ exports[`start servers CLI options rejects invalid options even if valid options functional_tests_server: invalid option [grep] ...stack trace... " -`; \ No newline at end of file +`; diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js index 3ceecb2806628..d63a8df2491a8 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.test.js @@ -126,6 +126,15 @@ describe('start servers CLI', () => { checkMockConsoleLogSnapshot(logMock); }); + it('accepts boolean value for updateSnapshots', async () => { + global.process.argv.push('--updateSnapshots'); + + await startServersCli('foo'); + + expect(exitMock).toHaveBeenCalledWith(1); + checkMockConsoleLogSnapshot(logMock); + }); + it('accepts source value for esFrom', async () => { global.process.argv.push('--esFrom', 'source'); diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 14883ac977c43..d9389c8cbc154 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -22,7 +22,7 @@ import { CliError } from './run_cli'; async function createFtr({ configPath, - options: { installDir, log, bail, grep, updateBaselines, suiteFiles, suiteTags }, + options: { installDir, log, bail, grep, updateBaselines, suiteFiles, suiteTags, updateSnapshots }, }) { const config = await readConfigFile(log, configPath); @@ -37,6 +37,7 @@ async function createFtr({ installDir, }, updateBaselines, + updateSnapshots, suiteFiles: { include: [...suiteFiles.include, ...config.get('suiteFiles.include')], exclude: [...suiteFiles.exclude, ...config.get('suiteFiles.exclude')], diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 3219e6cf3d6ee..6d94389f82caa 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -5,6 +5,9 @@ "src/**/*", "index.d.ts" ], + "exclude": [ + "types/ftr_globals/**/*" + ], "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, diff --git a/test/mocha_decorations.d.ts b/packages/kbn-test/types/ftr_globals/mocha.d.ts similarity index 73% rename from test/mocha_decorations.d.ts rename to packages/kbn-test/types/ftr_globals/mocha.d.ts index 5ad289eb4f1a3..d143b742b6dd8 100644 --- a/test/mocha_decorations.d.ts +++ b/packages/kbn-test/types/ftr_globals/mocha.d.ts @@ -19,27 +19,11 @@ import { Suite } from 'mocha'; -type Tags = - | 'ciGroup1' - | 'ciGroup2' - | 'ciGroup3' - | 'ciGroup4' - | 'ciGroup5' - | 'ciGroup6' - | 'ciGroup7' - | 'ciGroup8' - | 'ciGroup9' - | 'ciGroup10' - | 'ciGroup11' - | 'ciGroup12'; - -// We need to use the namespace here to match the Mocha definition declare module 'mocha' { interface Suite { /** * Assign tags to the test suite to determine in which CI job it should be run. */ - tags(tags: T | T[]): void; - tags(tags: T | T[]): void; + tags(tags: string[] | string): void; } } diff --git a/packages/kbn-test/types/ftr_globals/snapshots.d.ts b/packages/kbn-test/types/ftr_globals/snapshots.d.ts new file mode 100644 index 0000000000000..ab247a72991eb --- /dev/null +++ b/packages/kbn-test/types/ftr_globals/snapshots.d.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare const expectSnapshot: ( + received: any +) => { + toMatch: () => void; + toMatchInline: (_actual?: any) => void; +}; diff --git a/test/tsconfig.json b/test/tsconfig.json index 390e0b88c3d5c..df26441b0806f 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,7 +4,7 @@ "incremental": false, "types": ["node", "mocha", "flot"] }, - "include": ["**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*"], + "include": ["**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*", "../packages/kbn-test/types/ftr_globals/**/*"], "exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], "references": [ { "path": "../src/core/tsconfig.json" }, diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index 6f410add0fa4d..f59b79a6b7bfc 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -9,15 +9,12 @@ import { settingsObjectId, settingsObjectType, } from '../../../../../plugins/uptime/server/lib/saved_objects'; -import { registerMochaHooksForSnapshots } from '../../../../apm_api_integration/common/match_snapshot'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const server = getService('kibanaServer'); describe('uptime REST endpoints', () => { - registerMochaHooksForSnapshots(); - beforeEach('clear settings', async () => { try { await server.savedObjects.delete({ diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index 08a339ed59326..bdc18ac831d27 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -9,7 +9,6 @@ import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { MonitorSummariesResultType } from '../../../../../plugins/uptime/common/runtime_types'; import { API_URLS } from '../../../../../plugins/uptime/common/constants'; -import { expectSnapshot } from '../../../../apm_api_integration/common/match_snapshot'; interface ExpectedMonitorStatesPage { response: any; diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts index 0a730217e53f5..751ee8753c449 100644 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts +++ b/x-pack/test/apm_api_integration/basic/tests/correlations/ranges.ts @@ -9,7 +9,6 @@ import { format } from 'url'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import archives_metadata from '../../../common/archives_metadata'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { expectSnapshot } from '../../../common/match_snapshot'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts index 0cfdf3ec474d5..3cf1c2cecb42b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts +++ b/x-pack/test/apm_api_integration/basic/tests/correlations/slow_durations.ts @@ -9,7 +9,6 @@ import { format } from 'url'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import archives_metadata from '../../../common/archives_metadata'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { expectSnapshot } from '../../../common/match_snapshot'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 39dd721c7067e..0381e5f51bb9b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registerMochaHooksForSnapshots } from '../../common/match_snapshot'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM specs (basic)', function () { - registerMochaHooksForSnapshots(); - this.tags('ciGroup1'); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts index cae562b3f5dc5..d52aa2727d651 100644 --- a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts @@ -8,7 +8,6 @@ import { first } from 'lodash'; import { MetricsChartsByAgentAPIResponse } from '../../../../../plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent'; import { GenericMetricsChart } from '../../../../../plugins/apm/server/lib/metrics/transform_metrics_chart'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { expectSnapshot } from '../../../common/match_snapshot'; interface ChartResponse { body: MetricsChartsByAgentAPIResponse; diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts index 01fa09630e85a..cdeab9ecbdc49 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts index d729680154c1d..3820a76651053 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_maps/service_maps.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import archives_metadata from '../../../common/archives_metadata'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts index b699a30d40418..088b7cb8bb568 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import qs from 'querystring'; import { pick, uniqBy } from 'lodash'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import archives from '../../../common/archives_metadata'; diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index cd2bdb7fde19e..4d70c4e949433 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { isEmpty, pick } from 'lodash'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts index 1221ce0198d82..40b6db6997f8a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts b/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts index 32a06b8fb880e..bde9364efc685 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/agent_configuration.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { omit, orderBy } from 'lodash'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { AgentConfigurationIntake } from '../../../../../plugins/apm/common/agent_configuration/configuration_types'; import { AgentConfigSearchParams } from '../../../../../plugins/apm/server/routes/settings/agent_configuration'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts index 2c8f13ce79f76..a9e6eae8bed88 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts index d1dbd15f4dced..4fa3e46430e91 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts index 60b4020e73dce..8ac5566fc2c49 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index 6a3a1ddd0f6ae..4dbd6cc4cd6f7 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts index f2e58718870bf..24f542c222d6e 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/distribution.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/distribution.ts index e0b03e1a91f40..a93aff5c8cf32 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/distribution.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/distribution.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import qs from 'querystring'; import { isEmpty } from 'lodash'; import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts index 86309c91b0bc2..da3d07a0e83a3 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { first, last } from 'lodash'; import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts index 2e802957a95e3..d4fdfe6d0fc76 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; function sortTransactionGroups(items: any[]) { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts index c3b969d765664..5ebbdfa16d9a8 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import archives_metadata from '../../../common/archives_metadata'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts b/x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts index 6235e7abd37ec..05c6439508ece 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/csm_services.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/has_rum_data.ts b/x-pack/test/apm_api_integration/trial/tests/csm/has_rum_data.ts index 12fdb5ba9704e..f2033e03f5821 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/has_rum_data.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/has_rum_data.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumHasDataApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts index 0edffe7999a65..6fc8cb4c1d4e1 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/long_task_metrics.ts b/x-pack/test/apm_api_integration/trial/tests/csm/long_task_metrics.ts index 518c4ef8a81a7..6db5de24baa99 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/long_task_metrics.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/long_task_metrics.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts index ca5670d41d8ee..5d910862843d5 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts index c887fa3e77648..961c783434639 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index 5dbe266deeb81..7e970493eb611 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index a67dd1bcbd7a8..97ab662313c7c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -5,14 +5,11 @@ */ import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; -import { registerMochaHooksForSnapshots } from '../../common/match_snapshot'; export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM specs (trial)', function () { this.tags('ciGroup1'); - registerMochaHooksForSnapshots(); - describe('Services', function () { loadTestFile(require.resolve('./services/annotations')); loadTestFile(require.resolve('./services/top_services.ts')); diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index 9c01833f78e5d..b1e29b220dd5c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { isEmpty, uniq } from 'lodash'; import archives_metadata from '../../../common/archives_metadata'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index bb611013351d7..90cad966ba102 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; diff --git a/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts b/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts index 47e465596e0d7..99e90b8433c84 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/transaction_groups_charts.ts @@ -5,7 +5,6 @@ */ import expect from '@kbn/expect'; -import { expectSnapshot } from '../../../common/match_snapshot'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; diff --git a/x-pack/test/mocha_decorations.d.ts b/x-pack/test/mocha_decorations.d.ts deleted file mode 100644 index 44f43a22de1f9..0000000000000 --- a/x-pack/test/mocha_decorations.d.ts +++ /dev/null @@ -1,17 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Suite } from 'mocha'; - -// We need to use the namespace here to match the Mocha definition -declare module 'mocha' { - interface Suite { - /** - * Assign tags to the test suite to determine in which CI job it should be run. - */ - tags(tags: string[] | string): void; - } -} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index e041292ebf3c9..3ac7026d16a17 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -5,7 +5,7 @@ "incremental": false, "types": ["mocha", "node", "flot"] }, - "include": ["**/*", "../typings/**/*"], + "include": ["**/*", "../typings/**/*", "../../packages/kbn-test/types/ftr_globals/**/*"], "exclude": ["../typings/jest.d.ts"], "references": [ { "path": "../../src/core/tsconfig.json" }, From 91540ce57b4738c0f8e15757216bbb7af46b75b3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 19 Nov 2020 14:45:36 +0000 Subject: [PATCH 02/16] skip flaky suite (#83793) --- .../cypress/integration/alerts_detection_rules_custom.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index d14e09d9384a2..83f1a02aceeb8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -216,7 +216,8 @@ describe.skip('Custom detection rules creation', () => { }); }); -describe('Custom detection rules deletion and edition', () => { +// FLAKY: https://github.com/elastic/kibana/issues/83793 +describe.skip('Custom detection rules deletion and edition', () => { beforeEach(() => { esArchiverLoad('custom_rules'); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); From 98a8ec0aa4f04d40543ac4fd34adc2f4548ca69a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 19 Nov 2020 14:48:51 +0000 Subject: [PATCH 03/16] skip flaky suite (#65278) --- .../cypress/integration/cases_connectors.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts index ed885ad653e5d..1bba390780264 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts @@ -17,7 +17,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { CASES_URL } from '../urls/navigation'; -describe('Cases connectors', () => { +// FLAKY: https://github.com/elastic/kibana/issues/65278 +describe.skip('Cases connectors', () => { before(() => { cy.server(); cy.route('POST', '**/api/actions/action').as('createConnector'); From 854e3a62aa9ca16606c2c189d614a772dea65b6c Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 19 Nov 2020 07:52:33 -0700 Subject: [PATCH 04/16] [7.x] [Maps] Add 'crossed' & 'exited' events to tracking alert (#82463) (#83737) --- .../geo_threshold/query_builder/index.tsx | 2 +- .../public/alert_types/geo_threshold/types.ts | 1 + .../geo_threshold/geo_threshold.ts | 56 +++++++++++-------- .../geo_threshold/tests/geo_threshold.test.ts | 53 ++++++++++++++++-- 4 files changed, 85 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index f138c08c0f993..41fd190b0e85d 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -277,7 +277,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent setAlertParams('trackingEvent', e.target.value)} - options={[conditionOptions[0]]} // TODO: Make all options avab. before merge + options={conditionOptions} /> diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts index 0358fcd66a467..746d73bf28627 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts @@ -7,6 +7,7 @@ export enum TrackingEvent { entered = 'entered', exited = 'exited', + crossed = 'crossed', } export interface GeoThresholdAlertParams { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts index e223cdb7ea545..6efa68a2e5a1e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts @@ -144,11 +144,14 @@ export function getMovedEntities( [] ) // Do not track entries to or exits from 'other' - .filter((entityMovementDescriptor: EntityMovementDescriptor) => - trackingEvent === 'entered' - ? entityMovementDescriptor.currLocation.shapeId !== OTHER_CATEGORY - : entityMovementDescriptor.prevLocation.shapeId !== OTHER_CATEGORY - ) + .filter((entityMovementDescriptor: EntityMovementDescriptor) => { + if (trackingEvent !== 'crossed') { + return trackingEvent === 'entered' + ? entityMovementDescriptor.currLocation.shapeId !== OTHER_CATEGORY + : entityMovementDescriptor.prevLocation.shapeId !== OTHER_CATEGORY; + } + return true; + }) ); } @@ -253,27 +256,36 @@ export const getGeoThresholdExecutor = (log: Logger) => movedEntities.forEach(({ entityName, currLocation, prevLocation }) => { const toBoundaryName = shapesIdsNamesMap[currLocation.shapeId] || currLocation.shapeId; const fromBoundaryName = shapesIdsNamesMap[prevLocation.shapeId] || prevLocation.shapeId; - services - .alertInstanceFactory(`${entityName}-${toBoundaryName || currLocation.shapeId}`) - .scheduleActions(ActionGroupId, { - entityId: entityName, - timeOfDetection: new Date(currIntervalEndTime).getTime(), - crossingLine: `LINESTRING (${prevLocation.location[0]} ${prevLocation.location[1]}, ${currLocation.location[0]} ${currLocation.location[1]})`, + let alertInstance; + if (params.trackingEvent === 'entered') { + alertInstance = `${entityName}-${toBoundaryName || currLocation.shapeId}`; + } else if (params.trackingEvent === 'exited') { + alertInstance = `${entityName}-${fromBoundaryName || prevLocation.shapeId}`; + } else { + // == 'crossed' + alertInstance = `${entityName}-${fromBoundaryName || prevLocation.shapeId}-${ + toBoundaryName || currLocation.shapeId + }`; + } + services.alertInstanceFactory(alertInstance).scheduleActions(ActionGroupId, { + entityId: entityName, + timeOfDetection: new Date(currIntervalEndTime).getTime(), + crossingLine: `LINESTRING (${prevLocation.location[0]} ${prevLocation.location[1]}, ${currLocation.location[0]} ${currLocation.location[1]})`, - toEntityLocation: `POINT (${currLocation.location[0]} ${currLocation.location[1]})`, - toEntityDateTime: currLocation.date, - toEntityDocumentId: currLocation.docId, + toEntityLocation: `POINT (${currLocation.location[0]} ${currLocation.location[1]})`, + toEntityDateTime: currLocation.date, + toEntityDocumentId: currLocation.docId, - toBoundaryId: currLocation.shapeId, - toBoundaryName, + toBoundaryId: currLocation.shapeId, + toBoundaryName, - fromEntityLocation: `POINT (${prevLocation.location[0]} ${prevLocation.location[1]})`, - fromEntityDateTime: prevLocation.date, - fromEntityDocumentId: prevLocation.docId, + fromEntityLocation: `POINT (${prevLocation.location[0]} ${prevLocation.location[1]})`, + fromEntityDateTime: prevLocation.date, + fromEntityDocumentId: prevLocation.docId, - fromBoundaryId: prevLocation.shapeId, - fromBoundaryName, - }); + fromBoundaryId: prevLocation.shapeId, + fromBoundaryName, + }); }); // Combine previous results w/ current results for state of next run diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts index e4cee9c677713..5b5197ac62a39 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts @@ -99,7 +99,6 @@ describe('geo_threshold', () => { }); describe('getMovedEntities', () => { - const trackingEvent = 'entered'; it('should return empty array if only movements were within same shapes', async () => { const currLocationArr = [ { @@ -133,7 +132,7 @@ describe('geo_threshold', () => { shapeLocationId: 'sameShape2', }, ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, trackingEvent); + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); expect(movedEntities).toEqual([]); }); @@ -170,7 +169,7 @@ describe('geo_threshold', () => { shapeLocationId: 'thisOneDidntMove', }, ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, trackingEvent); + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); expect(movedEntities.length).toEqual(1); }); @@ -193,7 +192,7 @@ describe('geo_threshold', () => { shapeLocationId: 'oldShapeLocation', }, ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, trackingEvent); + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); expect(movedEntities).toEqual([]); }); @@ -219,5 +218,51 @@ describe('geo_threshold', () => { const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'exited'); expect(movedEntities).toEqual([]); }); + + it('should not ignore "crossed" results from "other"', async () => { + const currLocationArr = [ + { + dateInShape: '2020-09-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 41.62806099653244], + shapeLocationId: 'newShapeLocation', + }, + ]; + const prevLocationArr = [ + { + dateInShape: '2020-08-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 40.62806099653244], + shapeLocationId: OTHER_CATEGORY, + }, + ]; + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'crossed'); + expect(movedEntities.length).toEqual(1); + }); + + it('should not ignore "crossed" results to "other"', async () => { + const currLocationArr = [ + { + dateInShape: '2020-08-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 40.62806099653244], + shapeLocationId: OTHER_CATEGORY, + }, + ]; + const prevLocationArr = [ + { + dateInShape: '2020-09-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 41.62806099653244], + shapeLocationId: 'newShapeLocation', + }, + ]; + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'crossed'); + expect(movedEntities.length).toEqual(1); + }); }); }); From d6c5323766f7418691b6e35309da5cd1e4e0adb0 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 19 Nov 2020 07:52:51 -0700 Subject: [PATCH 05/16] [7.x] [Maps] Add query bar inputs to geo threshold alerts tracked points & boundaries (#80871) (#83727) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/stack_alerts/kibana.json | 2 +- ...eshold_alert_type_expression.test.tsx.snap | 210 ++++++++++++++++++ ...o_threshold_alert_type_expression.test.tsx | 94 ++++++++ .../geo_threshold/query_builder/index.tsx | 67 ++++++ .../public/alert_types/geo_threshold/types.ts | 4 + .../alert_types/geo_threshold/alert_type.ts | 5 + .../geo_threshold/es_query_builder.ts | 80 +++++-- .../geo_threshold/geo_threshold.ts | 3 +- .../tests/es_query_builder.test.ts | 67 ++++++ .../plugins/triggers_actions_ui/kibana.json | 4 +- .../public/application/app.tsx | 8 +- .../public/application/app_context.tsx | 7 +- .../public/application/boot.tsx | 9 +- .../actions_connectors_list.test.tsx | 10 +- .../components/alert_details.test.tsx | 2 +- .../components/alert_details.tsx | 8 +- .../sections/alert_form/alert_add.test.tsx | 2 +- .../components/alerts_list.test.tsx | 8 +- .../alerts_list/components/alerts_list.tsx | 8 +- .../public/application/test_utils/index.ts | 12 +- .../triggers_actions_ui/public/index.ts | 1 + .../triggers_actions_ui/public/plugin.ts | 6 +- 22 files changed, 559 insertions(+), 58 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index b7405c38d1611..884d33ef669e5 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact"], + "requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact", "savedObjects", "data"], "configPath": ["xpack", "stack_alerts"], "ui": true } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap new file mode 100644 index 0000000000000..dae168417b0bc --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap @@ -0,0 +1,210 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render BoundaryIndexExpression 1`] = ` + + + + + + + + + + + + } +/> +`; + +exports[`should render EntityIndexExpression 1`] = ` + + + + + + } + labelType="label" + > + + + + + + + } +/> +`; + +exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` + + + + + + } + labelType="label" + > + + + + + + + } +/> +`; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx new file mode 100644 index 0000000000000..d115dbeb76e37 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EntityIndexExpression } from './expressions/entity_index_expression'; +import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; +import { ApplicationStart, DocLinksStart, HttpSetup, ToastsStart } from 'kibana/public'; +import { + ActionTypeRegistryContract, + AlertTypeRegistryContract, + IErrorObject, +} from '../../../../../triggers_actions_ui/public'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +const alertsContext = { + http: (null as unknown) as HttpSetup, + alertTypeRegistry: (null as unknown) as AlertTypeRegistryContract, + actionTypeRegistry: (null as unknown) as ActionTypeRegistryContract, + toastNotifications: (null as unknown) as ToastsStart, + docLinks: (null as unknown) as DocLinksStart, + capabilities: (null as unknown) as ApplicationStart['capabilities'], +}; + +const alertParams = { + index: '', + indexId: '', + geoField: '', + entity: '', + dateField: '', + trackingEvent: '', + boundaryType: '', + boundaryIndexTitle: '', + boundaryIndexId: '', + boundaryGeoField: '', +}; + +test('should render EntityIndexExpression', async () => { + const component = shallow( + {}} + setAlertParamsGeoField={() => {}} + setAlertProperty={() => {}} + setIndexPattern={() => {}} + indexPattern={('' as unknown) as IIndexPattern} + isInvalid={false} + /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render EntityIndexExpression w/ invalid flag if invalid', async () => { + const component = shallow( + {}} + setAlertParamsGeoField={() => {}} + setAlertProperty={() => {}} + setIndexPattern={() => {}} + indexPattern={('' as unknown) as IIndexPattern} + isInvalid={true} + /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render BoundaryIndexExpression', async () => { + const component = shallow( + {}} + setBoundaryGeoField={() => {}} + setBoundaryNameField={() => {}} + boundaryNameField={'testNameField'} + /> + ); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index 41fd190b0e85d..c573d3b738373 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -30,6 +30,12 @@ import { EntityIndexExpression } from './expressions/entity_index_expression'; import { EntityByExpression } from './expressions/entity_by_expression'; import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { + esQuery, + esKuery, + Query, + QueryStringInput, +} from '../../../../../../../src/plugins/data/public'; const DEFAULT_VALUES = { TRACKING_EVENT: '', @@ -67,6 +73,18 @@ const labelForDelayOffset = ( ); +function validateQuery(query: Query) { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + query.language === 'kuery' + ? esKuery.fromKueryExpression(query.query) + : esQuery.luceneStringToDsl(query.query); + } catch (err) { + return false; + } + return true; +} + export const GeoThresholdAlertTypeExpression: React.FunctionComponent( + indexQuery || { + query: '', + language: 'kuery', + } + ); const [boundaryIndexPattern, _setBoundaryIndexPattern] = useState({ id: '', fields: [], @@ -118,6 +144,12 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent( + boundaryIndexQuery || { + query: '', + language: 'kuery', + } + ); const [delayOffset, _setDelayOffset] = useState(0); function setDelayOffset(_delayOffset: number) { setAlertParams('delayOffsetWithUnits', `${_delayOffset}${delayOffsetUnit}`); @@ -248,6 +280,23 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent + + + { + if (query.language) { + if (validateQuery(query)) { + setAlertParams('indexQuery', query); + } + setIndexQueryInput(query); + } + }} + /> + @@ -313,6 +362,24 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent + + + { + if (query.language) { + if (validateQuery(query)) { + setAlertParams('boundaryIndexQuery', query); + } + setBoundaryIndexQueryInput(query); + } + }} + /> + + ); }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts index 746d73bf28627..5ac9c7fd29317 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Query } from '../../../../../../src/plugins/data/common'; + export enum TrackingEvent { entered = 'entered', exited = 'exited', @@ -23,6 +25,8 @@ export interface GeoThresholdAlertParams { boundaryGeoField: string; boundaryNameField?: string; delayOffsetWithUnits?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; } // Will eventually include 'geo_shape' diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts index 9fc46fe2f2586..0c40f5b5f3866 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts @@ -15,6 +15,7 @@ import { ActionVariable, AlertTypeState, } from '../../../../alerts/server'; +import { Query } from '../../../../../../src/plugins/data/common/query'; export const GEO_THRESHOLD_ID = '.geo-threshold'; export type TrackingEvent = 'entered' | 'exited'; @@ -155,6 +156,8 @@ export const ParamsSchema = schema.object({ boundaryGeoField: schema.string({ minLength: 1 }), boundaryNameField: schema.maybe(schema.string({ minLength: 1 })), delayOffsetWithUnits: schema.maybe(schema.string({ minLength: 1 })), + indexQuery: schema.maybe(schema.any({})), + boundaryIndexQuery: schema.maybe(schema.any({})), }); export interface GeoThresholdParams { @@ -170,6 +173,8 @@ export interface GeoThresholdParams { boundaryGeoField: string; boundaryNameField?: string; delayOffsetWithUnits?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; } export function getAlertType( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts index 97be51b2a6256..02ac19e7b6f1e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts @@ -7,6 +7,13 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Logger } from 'src/core/server'; +import { + Query, + IIndexPattern, + fromKueryExpression, + toElasticsearchQuery, + luceneStringToDsl, +} from '../../../../../../src/plugins/data/common'; export const OTHER_CATEGORY = 'other'; // Consider dynamically obtaining from config? @@ -14,6 +21,19 @@ const MAX_TOP_LEVEL_QUERY_SIZE = 0; const MAX_SHAPES_QUERY_SIZE = 10000; const MAX_BUCKETS_LIMIT = 65535; +export const getEsFormattedQuery = (query: Query, indexPattern?: IIndexPattern) => { + let esFormattedQuery; + + const queryLanguage = query.language; + if (queryLanguage === 'kuery') { + const ast = fromKueryExpression(query.query); + esFormattedQuery = toElasticsearchQuery(ast, indexPattern); + } else { + esFormattedQuery = luceneStringToDsl(query.query); + } + return esFormattedQuery; +}; + export async function getShapesFilters( boundaryIndexTitle: string, boundaryGeoField: string, @@ -21,7 +41,8 @@ export async function getShapesFilters( callCluster: ILegacyScopedClusterClient['callAsCurrentUser'], log: Logger, alertId: string, - boundaryNameField?: string + boundaryNameField?: string, + boundaryIndexQuery?: Query ) { const filters: Record = {}; const shapesIdsNamesMap: Record = {}; @@ -30,8 +51,10 @@ export async function getShapesFilters( index: boundaryIndexTitle, body: { size: MAX_SHAPES_QUERY_SIZE, + ...(boundaryIndexQuery ? { query: getEsFormattedQuery(boundaryIndexQuery) } : {}), }, }); + boundaryData.hits.hits.forEach(({ _index, _id }) => { filters[_id] = { geo_shape: { @@ -66,6 +89,7 @@ export async function executeEsQueryFactory( boundaryGeoField, geoField, boundaryIndexTitle, + indexQuery, }: { entity: string; index: string; @@ -74,6 +98,7 @@ export async function executeEsQueryFactory( geoField: string; boundaryIndexTitle: string; boundaryNameField?: string; + indexQuery?: Query; }, { callCluster }: { callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] }, log: Logger, @@ -83,6 +108,19 @@ export async function executeEsQueryFactory( gteDateTime: Date | null, ltDateTime: Date | null ): Promise | undefined> => { + let esFormattedQuery; + if (indexQuery) { + const gteEpochDateTime = gteDateTime ? new Date(gteDateTime).getTime() : null; + const ltEpochDateTime = ltDateTime ? new Date(ltDateTime).getTime() : null; + const dateRangeUpdatedQuery = + indexQuery.language === 'kuery' + ? `(${dateField} >= "${gteEpochDateTime}" and ${dateField} < "${ltEpochDateTime}") and (${indexQuery.query})` + : `(${dateField}:[${gteDateTime} TO ${ltDateTime}]) AND (${indexQuery.query})`; + esFormattedQuery = getEsFormattedQuery({ + query: dateRangeUpdatedQuery, + language: indexQuery.language, + }); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any const esQuery: Record = { index, @@ -120,27 +158,29 @@ export async function executeEsQueryFactory( }, }, }, - query: { - bool: { - must: [], - filter: [ - { - match_all: {}, - }, - { - range: { - [dateField]: { - ...(gteDateTime ? { gte: gteDateTime } : {}), - lt: ltDateTime, // 'less than' to prevent overlap between intervals - format: 'strict_date_optional_time', + query: esFormattedQuery + ? esFormattedQuery + : { + bool: { + must: [], + filter: [ + { + match_all: {}, }, - }, + { + range: { + [dateField]: { + ...(gteDateTime ? { gte: gteDateTime } : {}), + lt: ltDateTime, // 'less than' to prevent overlap between intervals + format: 'strict_date_optional_time', + }, + }, + }, + ], + should: [], + must_not: [], }, - ], - should: [], - must_not: [], - }, - }, + }, stored_fields: ['*'], docvalue_fields: [ { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts index 6efa68a2e5a1e..5cb4156e84623 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts @@ -197,7 +197,8 @@ export const getGeoThresholdExecutor = (log: Logger) => services.callCluster, log, alertId, - params.boundaryNameField + params.boundaryNameField, + params.boundaryIndexQuery ); const executeEsQuery = await executeEsQueryFactory(params, services, log, shapesFilters); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts new file mode 100644 index 0000000000000..d577a88e8e2f8 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getEsFormattedQuery } from '../es_query_builder'; + +describe('esFormattedQuery', () => { + it('lucene queries are converted correctly', async () => { + const testLuceneQuery1 = { + query: `"airport": "Denver"`, + language: 'lucene', + }; + const esFormattedQuery1 = getEsFormattedQuery(testLuceneQuery1); + expect(esFormattedQuery1).toStrictEqual({ query_string: { query: '"airport": "Denver"' } }); + const testLuceneQuery2 = { + query: `title:"Fun with turnips" AND text:Cabbage, cabbage and more cabbage!`, + language: 'lucene', + }; + const esFormattedQuery2 = getEsFormattedQuery(testLuceneQuery2); + expect(esFormattedQuery2).toStrictEqual({ + query_string: { + query: `title:"Fun with turnips" AND text:Cabbage, cabbage and more cabbage!`, + }, + }); + }); + + it('kuery queries are converted correctly', async () => { + const testKueryQuery1 = { + query: `"airport": "Denver"`, + language: 'kuery', + }; + const esFormattedQuery1 = getEsFormattedQuery(testKueryQuery1); + expect(esFormattedQuery1).toStrictEqual({ + bool: { minimum_should_match: 1, should: [{ match_phrase: { airport: 'Denver' } }] }, + }); + const testKueryQuery2 = { + query: `"airport": "Denver" and ("animal": "goat" or "animal": "narwhal")`, + language: 'kuery', + }; + const esFormattedQuery2 = getEsFormattedQuery(testKueryQuery2); + expect(esFormattedQuery2).toStrictEqual({ + bool: { + filter: [ + { bool: { should: [{ match_phrase: { airport: 'Denver' } }], minimum_should_match: 1 } }, + { + bool: { + should: [ + { + bool: { should: [{ match_phrase: { animal: 'goat' } }], minimum_should_match: 1 }, + }, + { + bool: { + should: [{ match_phrase: { animal: 'narwhal' } }], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 9d79ab9232bf3..ab2d6c6a3c400 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -4,8 +4,8 @@ "server": true, "ui": true, "optionalPlugins": ["alerts", "features", "home"], - "requiredPlugins": ["management", "charts", "data"], + "requiredPlugins": ["management", "charts", "data", "kibanaReact", "savedObjects"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], - "requiredBundles": ["home", "alerts", "esUiShared"] + "requiredBundles": ["home", "alerts", "esUiShared", "kibanaReact", "kibanaUtils"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 5c1e0aa0100e8..fa38c4501379f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -15,6 +15,7 @@ import { ChromeBreadcrumb, CoreStart, ScopedHistory, + SavedObjectsClientContract, } from 'kibana/public'; import { KibanaFeature } from '../../../features/common'; import { Section, routeToAlertDetails } from './constants'; @@ -24,6 +25,7 @@ import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerts/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; const TriggersActionsUIHome = lazy(async () => import('./home')); const AlertDetailsRoute = lazy( @@ -31,13 +33,14 @@ const AlertDetailsRoute = lazy( ); export interface AppDeps { - dataPlugin: DataPublicPluginStart; + data: DataPublicPluginStart; charts: ChartsPluginStart; chrome: ChromeStart; alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; docLinks: DocLinksStart; toastNotifications: ToastsSetup; + storage?: Storage; http: HttpSetup; uiSettings: IUiSettingsClient; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; @@ -45,6 +48,9 @@ export interface AppDeps { actionTypeRegistry: ActionTypeRegistryContract; alertTypeRegistry: AlertTypeRegistryContract; history: ScopedHistory; + savedObjects?: { + client: SavedObjectsClientContract; + }; kibanaFeatures: KibanaFeature[]; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx index bf2e0c7274e7b..a4568d069c21c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx @@ -5,6 +5,7 @@ */ import React, { createContext, useContext } from 'react'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { AppDeps } from './app'; const AppContext = createContext(null); @@ -16,7 +17,11 @@ export const AppContextProvider = ({ appDeps: AppDeps | null; children: React.ReactNode; }) => { - return appDeps ? {children} : null; + return appDeps ? ( + + {children} + + ) : null; }; export const useAppDependencies = (): AppDeps => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx index bb46fd02a98a9..e18bf4ce84871 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx @@ -6,21 +6,20 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { SavedObjectsClientContract } from 'src/core/public'; - import { App, AppDeps } from './app'; import { setSavedObjectsClient } from '../common/lib/data_apis'; interface BootDeps extends AppDeps { element: HTMLElement; - savedObjects: SavedObjectsClientContract; I18nContext: any; } export const boot = (bootDeps: BootDeps) => { - const { I18nContext, element, savedObjects, ...appDeps } = bootDeps; + const { I18nContext, element, ...appDeps } = bootDeps; - setSavedObjectsClient(savedObjects); + if (appDeps.savedObjects) { + setSavedObjectsClient(appDeps.savedObjects.client); + } render( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 65d5389078880..71e1c60a92aed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -55,7 +55,7 @@ describe('actions_connectors_list component empty', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -165,7 +165,7 @@ describe('actions_connectors_list component with items', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -256,7 +256,7 @@ describe('actions_connectors_list component empty with show only capability', () const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -348,7 +348,7 @@ describe('actions_connectors_list with show only capability', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -452,7 +452,7 @@ describe('actions_connectors_list component with disabled items', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, injectedMetadata: mockes.injectedMetadata, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 70b6fb0b750dd..c2a7635b4cf96 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -42,7 +42,7 @@ jest.mock('../../../app_context', () => ({ toastNotifications: mockes.notifications.toasts, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, uiSettings: mockes.uiSettings, - dataPlugin: jest.fn(), + data: jest.fn(), charts: jest.fn(), })), })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 603058e6fcb52..b38f0e749a28d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -70,7 +70,7 @@ export const AlertDetails: React.FunctionComponent = ({ uiSettings, docLinks, charts, - dataPlugin, + data, setBreadcrumbs, chrome, } = useAppDependencies(); @@ -162,11 +162,11 @@ export const AlertDetails: React.FunctionComponent = ({ uiSettings, docLinks, charts, - dataFieldsFormats: dataPlugin.fieldFormats, + dataFieldsFormats: data.fieldFormats, reloadAlerts: setAlert, capabilities, - dataUi: dataPlugin.ui, - dataIndexPatterns: dataPlugin.indexPatterns, + dataUi: data.ui, + dataIndexPatterns: data.indexPatterns, }} > { toastNotifications: mocks.notifications.toasts, http: mocks.http, uiSettings: mocks.uiSettings, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), actionTypeRegistry, alertTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 611846cf4a521..a29c112b536fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -108,7 +108,7 @@ describe('alerts_list component empty', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -279,7 +279,7 @@ describe('alerts_list component with items', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -362,7 +362,7 @@ describe('alerts_list component empty with show only capability', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -483,7 +483,7 @@ describe('alerts_list with show only capability', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 75f359888a858..11d6f3470fec2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -83,7 +83,7 @@ export const AlertsList: React.FunctionComponent = () => { uiSettings, docLinks, charts, - dataPlugin, + data, kibanaFeatures, } = useAppDependencies(); const canExecuteActions = hasExecuteActionsCapability(capabilities); @@ -668,10 +668,10 @@ export const AlertsList: React.FunctionComponent = () => { uiSettings, docLinks, charts, - dataFieldsFormats: dataPlugin.fieldFormats, + dataFieldsFormats: data.fieldFormats, capabilities, - dataUi: dataPlugin.ui, - dataIndexPatterns: dataPlugin.indexPatterns, + dataUi: data.ui, + dataIndexPatterns: data.indexPatterns, kibanaFeatures, }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts index b5ab53d868cf1..061f3faaa6c0f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts @@ -26,20 +26,20 @@ export async function getMockedAppDependencies() { const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); return { + data: dataPluginMock.createStartContract(), + charts: chartPluginMock.createStartContract(), chrome, + navigateToApp, docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - alerting: alertingPluginMock.createStartContract(), toastNotifications: coreSetupMock.notifications.toasts, http: coreSetupMock.http, uiSettings: coreSetupMock.uiSettings, - navigateToApp, - capabilities, - history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), + capabilities, actionTypeRegistry, alertTypeRegistry, + history: scopedHistoryMock.create(), + alerting: alertingPluginMock.createStartContract(), kibanaFeatures, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 3794112e1d502..3187451d2600e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -17,6 +17,7 @@ export { AlertTypeModel, ActionType, ActionTypeRegistryContract, + AlertTypeRegistryContract, AlertTypeParamsExpressionProps, ValidationResult, ActionVariable, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 2d93d368ad8e5..a30747afe6914 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -22,6 +22,7 @@ import { import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; @@ -102,16 +103,17 @@ export class Plugin const { boot } = await import('./application/boot'); const kibanaFeatures = await pluginsStart.features.getFeatures(); return boot({ - dataPlugin: pluginsStart.data, + data: pluginsStart.data, charts: pluginsStart.charts, alerts: pluginsStart.alerts, element: params.element, toastNotifications: coreStart.notifications.toasts, + storage: new Storage(window.localStorage), http: coreStart.http, uiSettings: coreStart.uiSettings, docLinks: coreStart.docLinks, chrome: coreStart.chrome, - savedObjects: coreStart.savedObjects.client, + savedObjects: coreStart.savedObjects, I18nContext: coreStart.i18n.Context, capabilities: coreStart.application.capabilities, navigateToApp: coreStart.application.navigateToApp, From f31848a3dc511365ad9b4f7de572ef6abd0e5840 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 19 Nov 2020 14:51:45 +0000 Subject: [PATCH 06/16] skip flaky suite (#83771) --- .../cypress/integration/alerts_timeline.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index 31d8e4666d91d..c28c4e842e08b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -17,7 +17,8 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -describe('Alerts timeline', () => { +// FLAKY: https://github.com/elastic/kibana/issues/83771 +describe.skip('Alerts timeline', () => { beforeEach(() => { esArchiverLoad('timeline_alerts'); loginAndWaitForPage(DETECTIONS_URL); From 5956b9d02e0b50a74c73d13683ad57f754b04f7f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 19 Nov 2020 14:56:34 +0000 Subject: [PATCH 07/16] skip flaky suite (#83773) --- .../security_solution/cypress/integration/alerts.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index db841d2a732c4..36dc38b684742 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,7 +30,8 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -describe('Alerts', () => { +// FLAKY: https://github.com/elastic/kibana/issues/83773 +describe.skip('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); From 3004ac935c7bc440b336582ecda16085f666d0cb Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 19 Nov 2020 15:11:40 +0000 Subject: [PATCH 08/16] fixed pagination in connectors list (#83638) (#83790) Ensures we specify the page on the EuiTable so that pagination is retain after rerenders. --- .../actions_connectors_list.test.tsx | 88 +++++++++++++------ .../components/actions_connectors_list.tsx | 12 ++- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 71e1c60a92aed..226b9de8b677f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -16,6 +16,8 @@ import { chartPluginMock } from '../../../../../../../../src/plugins/charts/publ import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; import { featuresPluginMock } from '../../../../../../features/public/mocks'; +import { ActionConnector } from '../../../../types'; +import { times } from 'lodash'; jest.mock('../../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -109,36 +111,38 @@ describe('actions_connectors_list component empty', () => { describe('actions_connectors_list component with items', () => { let wrapper: ReactWrapper; - async function setup() { + async function setup(actionConnectors?: ActionConnector[]) { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); - loadAllActions.mockResolvedValueOnce([ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - isPreconfigured: false, - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - isPreconfigured: false, - config: {}, - }, - { - id: '3', - actionTypeId: 'test2', - description: 'My preconfigured test 2', - referencedByCount: 1, - isPreconfigured: true, - config: {}, - }, - ]); + loadAllActions.mockResolvedValueOnce( + actionConnectors ?? [ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + isPreconfigured: false, + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + isPreconfigured: false, + config: {}, + }, + { + id: '3', + actionTypeId: 'test2', + description: 'My preconfigured test 2', + referencedByCount: 1, + isPreconfigured: true, + config: {}, + }, + ] + ); loadActionTypes.mockResolvedValueOnce([ { id: 'test', @@ -217,6 +221,36 @@ describe('actions_connectors_list component with items', () => { expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2); }); + it('supports pagination', async () => { + await setup( + times(15, (index) => ({ + id: `connector${index}`, + actionTypeId: 'test', + name: `My test ${index}`, + secrets: {}, + description: `My test ${index}`, + isPreconfigured: false, + referencedByCount: 1, + config: {}, + })) + ); + expect(wrapper.find('[data-test-subj="actionsTable"]').first().prop('pagination')) + .toMatchInlineSnapshot(` + Object { + "initialPageIndex": 0, + "pageIndex": 0, + } + `); + wrapper.find('[data-test-subj="pagination-button-1"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="actionsTable"]').first().prop('pagination')) + .toMatchInlineSnapshot(` + Object { + "initialPageIndex": 0, + "pageIndex": 1, + } + `); + }); + test('if select item for edit should render ConnectorEditFlyout', async () => { await setup(); await wrapper.find('[data-test-subj="edit1"]').first().simulate('click'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index ff5585cf04dbe..c5d0a6aae54fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -18,6 +18,7 @@ import { EuiToolTip, EuiButtonIcon, EuiEmptyPrompt, + Criteria, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; @@ -54,6 +55,7 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { const [actionTypesIndex, setActionTypesIndex] = useState(undefined); const [actions, setActions] = useState([]); + const [pageIndex, setPageIndex] = useState(0); const [selectedItems, setSelectedItems] = useState([]); const [isLoadingActionTypes, setIsLoadingActionTypes] = useState(false); const [isLoadingActions, setIsLoadingActions] = useState(false); @@ -233,7 +235,15 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { : '', })} data-test-subj="actionsTable" - pagination={true} + pagination={{ + initialPageIndex: 0, + pageIndex, + }} + onTableChange={({ page }: Criteria) => { + if (page) { + setPageIndex(page.index); + } + }} selection={ canDelete ? { From 1c5a49a20eef33a6c0bbc361c4c1489333c0ec92 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 19 Nov 2020 09:33:25 -0600 Subject: [PATCH 09/16] [index patterns] improve index pattern cache (#83368) (#83795) * cache index pattern promise, not index pattern --- .../index_patterns/_pattern_cache.ts | 4 +- .../index_patterns/index_patterns.test.ts | 49 ++++++++++++++++--- .../index_patterns/index_patterns.ts | 32 +++++++----- 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts index a3653bb529fa3..19fe7c7c26c79 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts @@ -20,8 +20,8 @@ import { IndexPattern } from './index_pattern'; export interface PatternCache { - get: (id: string) => IndexPattern; - set: (id: string, value: IndexPattern) => IndexPattern; + get: (id: string) => Promise | undefined; + set: (id: string, value: Promise) => Promise; clear: (id: string) => void; clearAll: () => void; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index bf4265b41fcfe..a64bfae4db345 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -40,6 +40,7 @@ function setDocsourcePayload(id: string | null, providedPayload: any) { describe('IndexPatterns', () => { let indexPatterns: IndexPatternsService; let savedObjectsClient: SavedObjectsClientCommon; + let SOClientGetDelay = 0; beforeEach(() => { const indexPatternObj = { id: 'id', version: 'a', attributes: { title: 'title' } }; @@ -49,11 +50,14 @@ describe('IndexPatterns', () => { ); savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise); savedObjectsClient.create = jest.fn(); - savedObjectsClient.get = jest.fn().mockImplementation(async (type, id) => ({ - id: object.id, - version: object.version, - attributes: object.attributes, - })); + savedObjectsClient.get = jest.fn().mockImplementation(async (type, id) => { + await new Promise((resolve) => setTimeout(resolve, SOClientGetDelay)); + return { + id: object.id, + version: object.version, + attributes: object.attributes, + }; + }); savedObjectsClient.update = jest .fn() .mockImplementation(async (type, id, body, { version }) => { @@ -88,6 +92,7 @@ describe('IndexPatterns', () => { }); test('does cache gets for the same id', async () => { + SOClientGetDelay = 1000; const id = '1'; setDocsourcePayload(id, { id: 'foo', @@ -97,10 +102,17 @@ describe('IndexPatterns', () => { }, }); - const indexPattern = await indexPatterns.get(id); + // make two requests before first can complete + const indexPatternPromise = indexPatterns.get(id); + indexPatterns.get(id); - expect(indexPattern).toBeDefined(); - expect(indexPattern).toBe(await indexPatterns.get(id)); + indexPatternPromise.then((indexPattern) => { + expect(savedObjectsClient.get).toBeCalledTimes(1); + expect(indexPattern).toBeDefined(); + }); + + expect(await indexPatternPromise).toBe(await indexPatterns.get(id)); + SOClientGetDelay = 0; }); test('savedObjectCache pre-fetches only title', async () => { @@ -212,4 +224,25 @@ describe('IndexPatterns', () => { expect(indexPatterns.savedObjectToSpec(savedObject)).toMatchSnapshot(); }); + + test('failed requests are not cached', async () => { + savedObjectsClient.get = jest + .fn() + .mockImplementation(async (type, id) => { + return { + id: object.id, + version: object.version, + attributes: object.attributes, + }; + }) + .mockRejectedValueOnce({}); + + const id = '1'; + + // failed request! + expect(indexPatterns.get(id)).rejects.toBeDefined(); + + // successful subsequent request + expect(async () => await indexPatterns.get(id)).toBeDefined(); + }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 7ea4632f481c7..dc756a1028408 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -361,17 +361,7 @@ export class IndexPatternsService { }; }; - /** - * Get an index pattern by id. Cache optimized - * @param id - */ - - get = async (id: string): Promise => { - const cache = indexPatternCache.get(id); - if (cache) { - return cache; - } - + private getSavedObjectAndInit = async (id: string): Promise => { const savedObject = await this.savedObjectsClient.get( savedObjectType, id @@ -427,7 +417,6 @@ export class IndexPatternsService { : {}; const indexPattern = await this.create(spec, true); - indexPatternCache.set(id, indexPattern); if (isSaveRequired) { try { this.updateSavedObject(indexPattern); @@ -477,6 +466,23 @@ export class IndexPatternsService { .then(() => this); } + /** + * Get an index pattern by id. Cache optimized + * @param id + */ + + get = async (id: string): Promise => { + const indexPatternPromise = + indexPatternCache.get(id) || indexPatternCache.set(id, this.getSavedObjectAndInit(id)); + + // don't cache failed requests + indexPatternPromise.catch(() => { + indexPatternCache.clear(id); + }); + + return indexPatternPromise; + }; + /** * Create a new index pattern instance * @param spec @@ -535,7 +541,7 @@ export class IndexPatternsService { id: indexPattern.id, }); indexPattern.id = response.id; - indexPatternCache.set(indexPattern.id, indexPattern); + indexPatternCache.set(indexPattern.id, Promise.resolve(indexPattern)); return indexPattern; } From 8157ef65275bb69d9cbfba54743e16b7cd23bd5d Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 19 Nov 2020 09:02:17 -0700 Subject: [PATCH 10/16] [data.search] Server-side background session service (#81099) (#83766) * [Search] Add request context and asScoped pattern * Update docs * Unify interface for getting search client * [WIP] [data.search] Server-side background session service * Update examples/search_examples/server/my_strategy.ts Co-authored-by: Anton Dosov * Review feedback * Fix checks * Add tapFirst and additional props for session * Fix CI * Fix security search * Fix test * Fix test for reals * Add restore method * Add code to search examples * Add restore and search using restored ID * Fix handling of preference and order of params * Trim & cleanup * Fix types * Review feedback * Add tests and remove handling of username * Update docs * Move utils to server * Review feedback * More review feedback * Regenerate docs * Review feedback * Doc changes Co-authored-by: Anton Dosov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Anton Dosov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...ns-data-public.isearchoptions.isrestore.md | 13 + ...ins-data-public.isearchoptions.isstored.md | 13 + ...ugin-plugins-data-public.isearchoptions.md | 2 + ...gins-data-public.isessionservice.delete.md | 13 + ...lugins-data-public.isessionservice.find.md | 13 + ...plugins-data-public.isessionservice.get.md | 13 + ...s-data-public.isessionservice.isrestore.md | 13 + ...ns-data-public.isessionservice.isstored.md | 13 + ...gin-plugins-data-public.isessionservice.md | 9 +- ...ins-data-public.isessionservice.restore.md | 2 +- ...lugins-data-public.isessionservice.save.md | 13 + ...gins-data-public.isessionservice.update.md | 13 + ...ns-data-server.isearchoptions.isrestore.md | 13 + ...ins-data-server.isearchoptions.isstored.md | 13 + ...ugin-plugins-data-server.isearchoptions.md | 2 + examples/search_examples/kibana.json | 2 +- .../data/common/search/session/index.ts | 1 + .../data/common/search/session/mocks.ts | 7 + .../data/common/search/session/status.ts | 26 ++ .../data/common/search/session/types.ts | 62 ++++- src/plugins/data/common/search/types.ts | 11 + src/plugins/data/common/utils/index.ts | 1 + .../data/common/utils/tap_first.test.ts | 30 +++ src/plugins/data/common/utils/tap_first.ts | 31 +++ src/plugins/data/public/public.api.md | 25 +- .../data/public/search/search_interceptor.ts | 19 +- .../data/public/search/session_service.ts | 69 +++++- .../saved_objects/background_session.ts | 56 +++++ .../data/server/saved_objects/index.ts | 1 + src/plugins/data/server/search/mocks.ts | 21 ++ .../data/server/search/routes/search.ts | 14 +- .../data/server/search/routes/session.test.ts | 119 +++++++++ .../data/server/search/routes/session.ts | 201 +++++++++++++++ .../data/server/search/search_service.ts | 44 +++- .../data/server/search/session/index.ts | 20 ++ .../search/session/session_service.test.ts | 233 ++++++++++++++++++ .../server/search/session/session_service.ts | 204 +++++++++++++++ .../data/server/search/session/utils.test.ts | 37 +++ .../data/server/search/session/utils.ts | 30 +++ src/plugins/data/server/server.api.md | 2 + src/plugins/embeddable/public/public.api.md | 4 +- .../public/search/search_interceptor.ts | 8 +- .../server/search/es_search_strategy.ts | 1 + 43 files changed, 1407 insertions(+), 30 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md create mode 100644 src/plugins/data/common/search/session/status.ts create mode 100644 src/plugins/data/common/utils/tap_first.test.ts create mode 100644 src/plugins/data/common/utils/tap_first.ts create mode 100644 src/plugins/data/server/saved_objects/background_session.ts create mode 100644 src/plugins/data/server/search/routes/session.test.ts create mode 100644 src/plugins/data/server/search/routes/session.ts create mode 100644 src/plugins/data/server/search/session/index.ts create mode 100644 src/plugins/data/server/search/session/session_service.test.ts create mode 100644 src/plugins/data/server/search/session/session_service.ts create mode 100644 src/plugins/data/server/search/session/utils.test.ts create mode 100644 src/plugins/data/server/search/session/utils.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md new file mode 100644 index 0000000000000..672d77719962f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) + +## ISearchOptions.isRestore property + +Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) + +Signature: + +```typescript +isRestore?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md new file mode 100644 index 0000000000000..0d2c173f351c8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) + +## ISearchOptions.isStored property + +Whether the session is already saved (i.e. sent to background) + +Signature: + +```typescript +isStored?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index 76d0914173447..5acd837495dac 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -15,6 +15,8 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | +| [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md new file mode 100644 index 0000000000000..eabb966160c4d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) + +## ISessionService.delete property + +Deletes a session + +Signature: + +```typescript +delete: (sessionId: string) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md new file mode 100644 index 0000000000000..58e2fea0e6fe9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) + +## ISessionService.find property + +Gets a list of saved sessions + +Signature: + +```typescript +find: (options: SearchSessionFindOptions) => Promise>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md new file mode 100644 index 0000000000000..a2dff2f18253b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) + +## ISessionService.get property + +Gets a saved session + +Signature: + +```typescript +get: (sessionId: string) => Promise>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md new file mode 100644 index 0000000000000..8d8cd1f31bb95 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) + +## ISessionService.isRestore property + +Whether the active session is restored (i.e. reusing previous search IDs) + +Signature: + +```typescript +isRestore: () => boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md new file mode 100644 index 0000000000000..db737880bb84e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) + +## ISessionService.isStored property + +Whether the active session is already saved (i.e. sent to background) + +Signature: + +```typescript +isStored: () => boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md index 174f9dbe66bf4..02c0a821e552d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md @@ -15,8 +15,15 @@ export interface ISessionService | Property | Type | Description | | --- | --- | --- | | [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void | Clears the active session. | +| [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) | (sessionId: string) => Promise<void> | Deletes a session | +| [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) | (options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>> | Gets a list of saved sessions | +| [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Gets a saved session | | [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined> | Returns the observable that emits an update every time the session ID changes | | [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined | Returns the active session ID | -| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => void | Restores existing session | +| [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) | () => boolean | Whether the active session is restored (i.e. reusing previous search IDs) | +| [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) | () => boolean | Whether the active session is already saved (i.e. sent to background) | +| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Restores existing session | +| [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) | (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Saves a session | | [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string | Starts a new session | +| [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) | (sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any> | Updates a session | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md index 857e85bbd30eb..96106a6ef7e2d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md @@ -9,5 +9,5 @@ Restores existing session Signature: ```typescript -restore: (sessionId: string) => void; +restore: (sessionId: string) => Promise>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md new file mode 100644 index 0000000000000..4ac4a96614467 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) + +## ISessionService.save property + +Saves a session + +Signature: + +```typescript +save: (name: string, url: string) => Promise>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md new file mode 100644 index 0000000000000..5e2ff53d58ab7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) + +## ISessionService.update property + +Updates a session + +Signature: + +```typescript +update: (sessionId: string, attributes: Partial) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md new file mode 100644 index 0000000000000..ae518e5a052fc --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) + +## ISearchOptions.isRestore property + +Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) + +Signature: + +```typescript +isRestore?: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md new file mode 100644 index 0000000000000..aceee7fd6df68 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) + +## ISearchOptions.isStored property + +Whether the session is already saved (i.e. sent to background) + +Signature: + +```typescript +isStored?: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index af96e1413ba0c..85847e1c61d25 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -15,6 +15,8 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | +| [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/examples/search_examples/kibana.json b/examples/search_examples/kibana.json index 9577ec353a4c9..07bb6a0b750e3 100644 --- a/examples/search_examples/kibana.json +++ b/examples/search_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["navigation", "data", "developerExamples"], + "requiredPlugins": ["navigation", "data", "developerExamples", "kibanaUtils"], "optionalPlugins": [], "requiredBundles": [] } diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts index d8f7b5091eb8f..0feb43f8f1d4b 100644 --- a/src/plugins/data/common/search/session/index.ts +++ b/src/plugins/data/common/search/session/index.ts @@ -17,4 +17,5 @@ * under the License. */ +export * from './status'; export * from './types'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts index 370faaa640c56..4604e15e4e93b 100644 --- a/src/plugins/data/common/search/session/mocks.ts +++ b/src/plugins/data/common/search/session/mocks.ts @@ -27,5 +27,12 @@ export function getSessionServiceMock(): jest.Mocked { restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), + isStored: jest.fn(), + isRestore: jest.fn(), + save: jest.fn(), + get: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), }; } diff --git a/src/plugins/data/common/search/session/status.ts b/src/plugins/data/common/search/session/status.ts new file mode 100644 index 0000000000000..1f6b6eb3084bb --- /dev/null +++ b/src/plugins/data/common/search/session/status.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum BackgroundSessionStatus { + IN_PROGRESS = 'in_progress', + ERROR = 'error', + COMPLETE = 'complete', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index 6660b8395547f..d1ab22057695a 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -18,6 +18,7 @@ */ import { Observable } from 'rxjs'; +import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; export interface ISessionService { /** @@ -30,6 +31,17 @@ export interface ISessionService { * @returns `Observable` */ getSession$: () => Observable; + + /** + * Whether the active session is already saved (i.e. sent to background) + */ + isStored: () => boolean; + + /** + * Whether the active session is restored (i.e. reusing previous search IDs) + */ + isRestore: () => boolean; + /** * Starts a new session */ @@ -38,10 +50,58 @@ export interface ISessionService { /** * Restores existing session */ - restore: (sessionId: string) => void; + restore: (sessionId: string) => Promise>; /** * Clears the active session. */ clear: () => void; + + /** + * Saves a session + */ + save: (name: string, url: string) => Promise>; + + /** + * Gets a saved session + */ + get: (sessionId: string) => Promise>; + + /** + * Gets a list of saved sessions + */ + find: ( + options: SearchSessionFindOptions + ) => Promise>; + + /** + * Updates a session + */ + update: ( + sessionId: string, + attributes: Partial + ) => Promise; + + /** + * Deletes a session + */ + delete: (sessionId: string) => Promise; +} + +export interface BackgroundSessionSavedObjectAttributes { + name: string; + created: string; + expires: string; + status: string; + initialState: Record; + restoreState: Record; + idMapping: Record; +} + +export interface SearchSessionFindOptions { + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + filter?: string; } diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 7451edf5e2fa3..695ee34d3b468 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -92,4 +92,15 @@ export interface ISearchOptions { * A session ID, grouping multiple search requests into a single session. */ sessionId?: string; + + /** + * Whether the session is already saved (i.e. sent to background) + */ + isStored?: boolean; + + /** + * Whether the session is restored (i.e. search requests should re-use the stored search IDs, + * rather than starting from scratch) + */ + isRestore?: boolean; } diff --git a/src/plugins/data/common/utils/index.ts b/src/plugins/data/common/utils/index.ts index 8b8686c51b9c1..4b602cb963a8f 100644 --- a/src/plugins/data/common/utils/index.ts +++ b/src/plugins/data/common/utils/index.ts @@ -19,3 +19,4 @@ /** @internal */ export { shortenDottedString } from './shorten_dotted_string'; +export { tapFirst } from './tap_first'; diff --git a/src/plugins/data/common/utils/tap_first.test.ts b/src/plugins/data/common/utils/tap_first.test.ts new file mode 100644 index 0000000000000..033ae59f8c715 --- /dev/null +++ b/src/plugins/data/common/utils/tap_first.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { of } from 'rxjs'; +import { tapFirst } from './tap_first'; + +describe('tapFirst', () => { + it('should tap the first and only the first', () => { + const fn = jest.fn(); + of(1, 2, 3).pipe(tapFirst(fn)).subscribe(); + expect(fn).toBeCalledTimes(1); + expect(fn).lastCalledWith(1); + }); +}); diff --git a/src/plugins/data/common/utils/tap_first.ts b/src/plugins/data/common/utils/tap_first.ts new file mode 100644 index 0000000000000..2c783a3ef87f0 --- /dev/null +++ b/src/plugins/data/common/utils/tap_first.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { pipe } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +export function tapFirst(next: (x: T) => void) { + let isFirst = true; + return pipe( + tap((x: T) => { + if (isFirst) next(x); + isFirst = false; + }) + ); +} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 6c526ab613d76..057c89c4ca5c5 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -70,10 +70,12 @@ import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; -import { SavedObject } from 'src/core/server'; -import { SavedObject as SavedObject_2 } from 'src/core/public'; +import { SavedObject } from 'kibana/server'; +import { SavedObject as SavedObject_2 } from 'src/core/server'; +import { SavedObject as SavedObject_3 } from 'src/core/public'; import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectsFindResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; @@ -1395,7 +1397,7 @@ export class IndexPatternsService { // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts // // (undocumented) - getCache: () => Promise[] | null | undefined>; + getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts @@ -1409,7 +1411,7 @@ export class IndexPatternsService { // (undocumented) migrate(indexPattern: IndexPattern, newTitle: string): Promise; refreshFields: (indexPattern: IndexPattern) => Promise; - savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; + savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; setDefault: (id: string, force?: boolean) => Promise; updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; } @@ -1454,6 +1456,8 @@ export type ISearchGeneric = | undefined // @public (undocumented) export interface ISessionService { clear: () => void; + delete: (sessionId: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "SearchSessionFindOptions" needs to be exported by the entry point index.d.ts + find: (options: SearchSessionFindOptions) => Promise>; + get: (sessionId: string) => Promise>; getSession$: () => Observable; getSessionId: () => string | undefined; - restore: (sessionId: string) => void; + isRestore: () => boolean; + isStored: () => boolean; + // Warning: (ae-forgotten-export) The symbol "BackgroundSessionSavedObjectAttributes" needs to be exported by the entry point index.d.ts + restore: (sessionId: string) => Promise>; + save: (name: string, url: string) => Promise>; start: () => string; + update: (sessionId: string, attributes: Partial) => Promise; } // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2077,7 +2090,7 @@ export class SearchInterceptor { // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) - protected runSearch(request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string): Promise; + protected runSearch(request: IKibanaSearchRequest, options?: ISearchOptions): Promise; search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable; // @internal (undocumented) protected setupAbortSignal({ abortSignal, timeout, }: { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 78e65802bcf99..3fadb723b27cd 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -126,18 +126,25 @@ export class SearchInterceptor { */ protected runSearch( request: IKibanaSearchRequest, - signal: AbortSignal, - strategy?: string + options?: ISearchOptions ): Promise { const { id, ...searchRequest } = request; - const path = trimEnd(`/internal/search/${strategy || ES_SEARCH_STRATEGY}/${id || ''}`, '/'); - const body = JSON.stringify(searchRequest); + const path = trimEnd( + `/internal/search/${options?.strategy ?? ES_SEARCH_STRATEGY}/${id ?? ''}`, + '/' + ); + const body = JSON.stringify({ + sessionId: options?.sessionId, + isStored: options?.isStored, + isRestore: options?.isRestore, + ...searchRequest, + }); return this.deps.http.fetch({ method: 'POST', path, body, - signal, + signal: options?.abortSignal, }); } @@ -235,7 +242,7 @@ export class SearchInterceptor { abortSignal: options?.abortSignal, }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); - return from(this.runSearch(request, combinedSignal, options?.strategy)).pipe( + return from(this.runSearch(request, { ...options, abortSignal: combinedSignal })).pipe( catchError((e: Error) => { return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts index a172738812937..0141cff258a9f 100644 --- a/src/plugins/data/public/search/session_service.ts +++ b/src/plugins/data/public/search/session_service.ts @@ -19,9 +19,13 @@ import uuid from 'uuid'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { HttpStart, PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; import { ConfigSchema } from '../../config'; -import { ISessionService } from '../../common/search'; +import { + ISessionService, + BackgroundSessionSavedObjectAttributes, + SearchSessionFindOptions, +} from '../../common'; export class SessionService implements ISessionService { private session$ = new BehaviorSubject(undefined); @@ -30,6 +34,18 @@ export class SessionService implements ISessionService { } private appChangeSubscription$?: Subscription; private curApp?: string; + private http!: HttpStart; + + /** + * Has the session already been stored (i.e. "sent to background")? + */ + private _isStored: boolean = false; + + /** + * Is this session a restored session (have these requests already been made, and we're just + * looking to re-use the previous search IDs)? + */ + private _isRestore: boolean = false; constructor( initializerContext: PluginInitializerContext, @@ -39,6 +55,8 @@ export class SessionService implements ISessionService { Make sure that apps don't leave sessions open. */ getStartServices().then(([coreStart]) => { + this.http = coreStart.http; + this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { if (this.sessionId) { const message = `Application '${this.curApp}' had an open session while navigating`; @@ -69,16 +87,63 @@ export class SessionService implements ISessionService { return this.session$.asObservable(); } + public isStored() { + return this._isStored; + } + + public isRestore() { + return this._isRestore; + } + public start() { + this._isStored = false; + this._isRestore = false; this.session$.next(uuid.v4()); return this.sessionId!; } public restore(sessionId: string) { + this._isStored = true; + this._isRestore = true; this.session$.next(sessionId); + return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); } public clear() { + this._isStored = false; + this._isRestore = false; this.session$.next(undefined); } + + public async save(name: string, url: string) { + const response = await this.http.post(`/internal/session`, { + body: JSON.stringify({ + name, + url, + sessionId: this.sessionId, + }), + }); + this._isStored = true; + return response; + } + + public get(sessionId: string) { + return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); + } + + public find(options: SearchSessionFindOptions) { + return this.http.post(`/internal/session`, { + body: JSON.stringify(options), + }); + } + + public update(sessionId: string, attributes: Partial) { + return this.http.put(`/internal/session/${encodeURIComponent(sessionId)}`, { + body: JSON.stringify(attributes), + }); + } + + public delete(sessionId: string) { + return this.http.delete(`/internal/session/${encodeURIComponent(sessionId)}`); + } } diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/src/plugins/data/server/saved_objects/background_session.ts new file mode 100644 index 0000000000000..74b03c4d867e4 --- /dev/null +++ b/src/plugins/data/server/saved_objects/background_session.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsType } from 'kibana/server'; + +export const BACKGROUND_SESSION_TYPE = 'background-session'; + +export const backgroundSessionMapping: SavedObjectsType = { + name: BACKGROUND_SESSION_TYPE, + namespaceType: 'single', + hidden: true, + mappings: { + properties: { + name: { + type: 'keyword', + }, + created: { + type: 'date', + }, + expires: { + type: 'date', + }, + status: { + type: 'keyword', + }, + initialState: { + type: 'object', + enabled: false, + }, + restoreState: { + type: 'object', + enabled: false, + }, + idMapping: { + type: 'object', + enabled: false, + }, + }, + }, +}; diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 077f9380823d0..7cd4d319e6417 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -20,3 +20,4 @@ export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; export { kqlTelemetry } from './kql_telemetry'; export { searchTelemetry } from './search_telemetry'; +export { BACKGROUND_SESSION_TYPE, backgroundSessionMapping } from './background_session'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 4914726c85ef8..290e94ee7cf99 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,6 +17,8 @@ * under the License. */ +import type { RequestHandlerContext } from 'src/core/server'; +import { coreMock } from '../../../../core/server/mocks'; import { ISearchSetup, ISearchStart } from './types'; import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; @@ -40,3 +42,22 @@ export function createSearchStartMock(): jest.Mocked { searchSource: searchSourceMock.createStartContract(), }; } + +export function createSearchRequestHandlerContext(): jest.Mocked { + return { + core: coreMock.createRequestHandlerContext(), + search: { + search: jest.fn(), + cancel: jest.fn(), + session: { + save: jest.fn(), + get: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + trackId: jest.fn(), + getId: jest.fn(), + }, + }, + }; +} diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index a4161fe47b388..ed519164c8e43 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -35,11 +35,18 @@ export function registerSearchRoute(router: IRouter): void { query: schema.object({}, { unknowns: 'allow' }), - body: schema.object({}, { unknowns: 'allow' }), + body: schema.object( + { + sessionId: schema.maybe(schema.string()), + isStored: schema.maybe(schema.boolean()), + isRestore: schema.maybe(schema.boolean()), + }, + { unknowns: 'allow' } + ), }, }, async (context, request, res) => { - const searchRequest = request.body; + const { sessionId, isStored, isRestore, ...searchRequest } = request.body; const { strategy, id } = request.params; const abortSignal = getRequestAbortedSignal(request.events.aborted$); @@ -50,6 +57,9 @@ export function registerSearchRoute(router: IRouter): void { { abortSignal, strategy, + sessionId, + isStored, + isRestore, } ) .pipe(first()) diff --git a/src/plugins/data/server/search/routes/session.test.ts b/src/plugins/data/server/search/routes/session.test.ts new file mode 100644 index 0000000000000..f697f6d5a5c2b --- /dev/null +++ b/src/plugins/data/server/search/routes/session.test.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { CoreSetup, RequestHandlerContext } from 'kibana/server'; +import type { DataPluginStart } from '../../plugin'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { createSearchRequestHandlerContext } from '../mocks'; +import { registerSessionRoutes } from './session'; + +describe('registerSessionRoutes', () => { + let mockCoreSetup: MockedKeys>; + let mockContext: jest.Mocked; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + mockContext = createSearchRequestHandlerContext(); + registerSessionRoutes(mockCoreSetup.http.createRouter()); + }); + + it('save calls session.save with sessionId and attributes', async () => { + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const name = 'my saved background search session'; + const body = { sessionId, name }; + + const mockRequest = httpServerMock.createKibanaRequest({ body }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, saveHandler]] = mockRouter.post.mock.calls; + + saveHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.save).toHaveBeenCalledWith(sessionId, { name }); + }); + + it('get calls session.get with sessionId', async () => { + const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const params = { id }; + + const mockRequest = httpServerMock.createKibanaRequest({ params }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, getHandler]] = mockRouter.get.mock.calls; + + getHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.get).toHaveBeenCalledWith(id); + }); + + it('find calls session.find with options', async () => { + const page = 1; + const perPage = 5; + const sortField = 'my_field'; + const sortOrder = 'desc'; + const filter = 'foo: bar'; + const body = { page, perPage, sortField, sortOrder, filter }; + + const mockRequest = httpServerMock.createKibanaRequest({ body }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [, [, findHandler]] = mockRouter.post.mock.calls; + + findHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.find).toHaveBeenCalledWith(body); + }); + + it('update calls session.update with id and attributes', async () => { + const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const name = 'my saved background search session'; + const expires = new Date().toISOString(); + const params = { id }; + const body = { name, expires }; + + const mockRequest = httpServerMock.createKibanaRequest({ params, body }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, updateHandler]] = mockRouter.put.mock.calls; + + updateHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.update).toHaveBeenCalledWith(id, body); + }); + + it('delete calls session.delete with id', async () => { + const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const params = { id }; + + const mockRequest = httpServerMock.createKibanaRequest({ params }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, deleteHandler]] = mockRouter.delete.mock.calls; + + deleteHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.delete).toHaveBeenCalledWith(id); + }); +}); diff --git a/src/plugins/data/server/search/routes/session.ts b/src/plugins/data/server/search/routes/session.ts new file mode 100644 index 0000000000000..93f07ecfb92ff --- /dev/null +++ b/src/plugins/data/server/search/routes/session.ts @@ -0,0 +1,201 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerSessionRoutes(router: IRouter): void { + router.post( + { + path: '/internal/session', + validate: { + body: schema.object({ + sessionId: schema.string(), + name: schema.string(), + expires: schema.maybe(schema.string()), + initialState: schema.maybe(schema.object({}, { unknowns: 'allow' })), + restoreState: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + async (context, request, res) => { + const { sessionId, name, expires, initialState, restoreState } = request.body; + + try { + const response = await context.search!.session.save(sessionId, { + name, + expires, + initialState, + restoreState, + }); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.get( + { + path: '/internal/session/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, res) => { + const { id } = request.params; + try { + const response = await context.search!.session.get(id); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.post( + { + path: '/internal/session/_find', + validate: { + body: schema.object({ + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), + }), + }, + }, + async (context, request, res) => { + const { page, perPage, sortField, sortOrder, filter } = request.body; + try { + const response = await context.search!.session.find({ + page, + perPage, + sortField, + sortOrder, + filter, + }); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.delete( + { + path: '/internal/session/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, res) => { + const { id } = request.params; + try { + await context.search!.session.delete(id); + + return res.ok(); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.put( + { + path: '/internal/session/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + name: schema.maybe(schema.string()), + expires: schema.maybe(schema.string()), + }), + }, + }, + async (context, request, res) => { + const { id } = request.params; + const { name, expires } = request.body; + try { + const response = await context.search!.session.update(id, { name, expires }); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); +} diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index d8aa588719e3e..b44980164d097 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, from, Observable } from 'rxjs'; import { pick } from 'lodash'; import { CoreSetup, @@ -29,7 +29,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { first } from 'rxjs/operators'; +import { first, switchMap } from 'rxjs/operators'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ISearchSetup, @@ -49,7 +49,7 @@ import { DataPluginStart } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; -import { searchTelemetry } from '../saved_objects'; +import { BACKGROUND_SESSION_TYPE, searchTelemetry } from '../saved_objects'; import { IEsSearchRequest, IEsSearchResponse, @@ -70,10 +70,14 @@ import { } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; +import { BackgroundSessionService, ISearchSessionClient } from './session'; +import { registerSessionRoutes } from './routes/session'; +import { backgroundSessionMapping } from '../saved_objects'; +import { tapFirst } from '../../common/utils'; declare module 'src/core/server' { interface RequestHandlerContext { - search?: ISearchClient; + search?: ISearchClient & { session: ISearchSessionClient }; } } @@ -102,6 +106,7 @@ export class SearchService implements Plugin { private readonly searchSourceService = new SearchSourceService(); private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; + private sessionService: BackgroundSessionService = new BackgroundSessionService(); constructor( private initializerContext: PluginInitializerContext, @@ -121,12 +126,17 @@ export class SearchService implements Plugin { }; registerSearchRoute(router); registerMsearchRoute(router, routeDependencies); + registerSessionRoutes(router); core.http.registerRouteHandlerContext('search', async (context, request) => { const [coreStart] = await core.getStartServices(); - return this.asScopedProvider(coreStart)(request); + const search = this.asScopedProvider(coreStart)(request); + const session = this.sessionService.asScopedProvider(coreStart)(request); + return { ...search, session }; }); + core.savedObjects.registerType(backgroundSessionMapping); + this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider( @@ -223,6 +233,7 @@ export class SearchService implements Plugin { public stop() { this.aggsService.stop(); + this.sessionService.stop(); } private registerSearchStrategy = < @@ -248,7 +259,24 @@ export class SearchService implements Plugin { options.strategy ); - return strategy.search(searchRequest, options, deps); + // If this is a restored background search session, look up the ID using the provided sessionId + const getSearchRequest = async () => + !options.isRestore || searchRequest.id + ? searchRequest + : { + ...searchRequest, + id: await this.sessionService.getId(searchRequest, options, deps), + }; + + return from(getSearchRequest()).pipe( + switchMap((request) => strategy.search(request, options, deps)), + tapFirst((response) => { + if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; + this.sessionService.trackId(searchRequest, response.id, options, { + savedObjectsClient: deps.savedObjectsClient, + }); + }) + ); }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { @@ -273,7 +301,9 @@ export class SearchService implements Plugin { private asScopedProvider = ({ elasticsearch, savedObjects, uiSettings }: CoreStart) => { return (request: KibanaRequest): ISearchClient => { - const savedObjectsClient = savedObjects.getScopedClient(request); + const savedObjectsClient = savedObjects.getScopedClient(request, { + includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + }); const deps = { savedObjectsClient, esClient: elasticsearch.client.asScoped(request), diff --git a/src/plugins/data/server/search/session/index.ts b/src/plugins/data/server/search/session/index.ts new file mode 100644 index 0000000000000..11b5b16a02b56 --- /dev/null +++ b/src/plugins/data/server/search/session/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { BackgroundSessionService, ISearchSessionClient } from './session_service'; diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts new file mode 100644 index 0000000000000..1ceebae967d4c --- /dev/null +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -0,0 +1,233 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { BackgroundSessionStatus } from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { BackgroundSessionService } from './session_service'; +import { createRequestHash } from './utils'; + +describe('BackgroundSessionService', () => { + let savedObjectsClient: jest.Mocked; + let service: BackgroundSessionService; + + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + idMapping: {}, + }, + references: [], + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + service = new BackgroundSessionService(); + }); + + it('save throws if `name` is not provided', () => { + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('get calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const response = await service.get(sessionId, { savedObjectsClient }); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + it('find calls saved objects client', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find(options, { savedObjectsClient }); + + expect(response).toBe(mockResponse); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }); + + it('update calls saved objects client', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const attributes = { name: 'new_name' }; + const response = await service.update(sessionId, attributes, { savedObjectsClient }); + + expect(response).toBe(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }); + + it('delete calls saved objects client', async () => { + savedObjectsClient.delete.mockResolvedValue({}); + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const response = await service.delete(sessionId, { savedObjectsClient }); + + expect(response).toEqual({}); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + describe('trackId', () => { + it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const isStored = false; + const name = 'my saved background search session'; + const created = new Date().toISOString(); + const expires = new Date().toISOString(); + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + + await service.save(sessionId, { name, created, expires }, { savedObjectsClient }); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + { + name, + created, + expires, + initialState: {}, + restoreState: {}, + status: BackgroundSessionStatus.IN_PROGRESS, + idMapping: { [requestHash]: searchId }, + }, + { id: sessionId } + ); + }); + + it('updates saved object when `isStored` is `true`', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const isStored = true; + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { + idMapping: { [requestHash]: searchId }, + }); + }); + }); + + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, {}, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); + + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + + expect(() => + service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); + + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + + expect(() => + service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: false }, + { savedObjectsClient } + ) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); + + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const mockSession = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + idMapping: { [requestHash]: searchId }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + + const id = await service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: true }, + { savedObjectsClient } + ); + + expect(id).toBe(searchId); + }); + }); +}); diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts new file mode 100644 index 0000000000000..eca5f428b8555 --- /dev/null +++ b/src/plugins/data/server/search/session/session_service.ts @@ -0,0 +1,204 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { + BackgroundSessionSavedObjectAttributes, + IKibanaSearchRequest, + ISearchOptions, + SearchSessionFindOptions, + BackgroundSessionStatus, +} from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { createRequestHash } from './utils'; + +const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; + +export interface BackgroundSessionDependencies { + savedObjectsClient: SavedObjectsClientContract; +} + +export type ISearchSessionClient = ReturnType< + ReturnType +>; + +export class BackgroundSessionService { + /** + * Map of sessionId to { [requestHash]: searchId } + * @private + */ + private sessionSearchMap = new Map>(); + + constructor() {} + + public setup = () => {}; + + public start = (core: CoreStart) => { + return { + asScoped: this.asScopedProvider(core), + }; + }; + + public stop = () => { + this.sessionSearchMap.clear(); + }; + + // TODO: Generate the `userId` from the realm type/realm name/username + public save = async ( + sessionId: string, + { + name, + created = new Date().toISOString(), + expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), + status = BackgroundSessionStatus.IN_PROGRESS, + initialState = {}, + restoreState = {}, + }: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + if (!name) throw new Error('Name is required'); + + // Get the mapping of request hash/search ID for this session + const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); + const idMapping = Object.fromEntries(searchMap.entries()); + const attributes = { name, created, expires, status, initialState, restoreState, idMapping }; + const session = await savedObjectsClient.create( + BACKGROUND_SESSION_TYPE, + attributes, + { id: sessionId } + ); + + // Clear out the entries for this session ID so they don't get saved next time + this.sessionSearchMap.delete(sessionId); + + return session; + }; + + // TODO: Throw an error if this session doesn't belong to this user + public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.get( + BACKGROUND_SESSION_TYPE, + sessionId + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public find = ( + options: SearchSessionFindOptions, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.find({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public update = ( + sessionId: string, + attributes: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.update( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + }; + + /** + * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just + * store it in memory until a saved session exists. + * @internal + */ + public trackId = async ( + searchRequest: IKibanaSearchRequest, + searchId: string, + { sessionId, isStored }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId || !searchId) return; + const requestHash = createRequestHash(searchRequest.params); + + // If there is already a saved object for this session, update it to include this request/ID. + // Otherwise, just update the in-memory mapping for this session for when the session is saved. + if (isStored) { + const attributes = { idMapping: { [requestHash]: searchId } }; + await this.update(sessionId, attributes, deps); + } else { + const map = this.sessionSearchMap.get(sessionId) ?? new Map(); + map.set(requestHash, searchId); + this.sessionSearchMap.set(sessionId, map); + } + }; + + /** + * Look up an existing search ID that matches the given request in the given session so that the + * request can continue rather than restart. + * @internal + */ + public getId = async ( + searchRequest: IKibanaSearchRequest, + { sessionId, isStored, isRestore }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId) { + throw new Error('Session ID is required'); + } else if (!isStored) { + throw new Error('Cannot get search ID from a session that is not stored'); + } else if (!isRestore) { + throw new Error('Get search ID is only supported when restoring a session'); + } + + const session = await this.get(sessionId, deps); + const requestHash = createRequestHash(searchRequest.params); + if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { + throw new Error('No search ID in this session matching the given search request'); + } + + return session.attributes.idMapping[requestHash]; + }; + + public asScopedProvider = ({ savedObjects }: CoreStart) => { + return (request: KibanaRequest) => { + const savedObjectsClient = savedObjects.getScopedClient(request, { + includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + }); + const deps = { savedObjectsClient }; + return { + save: (sessionId: string, attributes: Partial) => + this.save(sessionId, attributes, deps), + get: (sessionId: string) => this.get(sessionId, deps), + find: (options: SearchSessionFindOptions) => this.find(options, deps), + update: (sessionId: string, attributes: Partial) => + this.update(sessionId, attributes, deps), + delete: (sessionId: string) => this.delete(sessionId, deps), + trackId: (searchRequest: IKibanaSearchRequest, searchId: string, options: ISearchOptions) => + this.trackId(searchRequest, searchId, options, deps), + getId: (searchRequest: IKibanaSearchRequest, options: ISearchOptions) => + this.getId(searchRequest, options, deps), + }; + }; + }; +} diff --git a/src/plugins/data/server/search/session/utils.test.ts b/src/plugins/data/server/search/session/utils.test.ts new file mode 100644 index 0000000000000..d190f892a7f84 --- /dev/null +++ b/src/plugins/data/server/search/session/utils.test.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createRequestHash } from './utils'; + +describe('data/search/session utils', () => { + describe('createRequestHash', () => { + it('ignores `preference`', () => { + const request = { + foo: 'bar', + }; + + const withPreference = { + ...request, + preference: 1234, + }; + + expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); + }); + }); +}); diff --git a/src/plugins/data/server/search/session/utils.ts b/src/plugins/data/server/search/session/utils.ts new file mode 100644 index 0000000000000..c3332f80b6e3f --- /dev/null +++ b/src/plugins/data/server/search/session/utils.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createHash } from 'crypto'; + +/** + * Generate the hash for this request so that, in the future, this hash can be used to look up + * existing search IDs for this request. Ignores the `preference` parameter since it generally won't + * match from one request to another identical request. + */ +export function createRequestHash(keys: Record) { + const { preference, ...params } = keys; + return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); +} diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 0ecf228463237..ff49db1ba414a 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -757,6 +757,8 @@ export class IndexPatternsService implements Plugin_3( - () => this.runSearch(request, combinedSignal, strategy), - (requestId) => this.runSearch({ ...request, id: requestId }, combinedSignal, strategy), + () => this.runSearch(request, { ...options, strategy, abortSignal: combinedSignal }), + (requestId) => + this.runSearch( + { ...request, id: requestId }, + { ...options, strategy, abortSignal: combinedSignal } + ), (r) => !r.isRunning, (response) => response.id, id, diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 53bcac02cb01d..2070610ceb20e 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -57,6 +57,7 @@ export const enhancedEsSearchStrategyProvider = ( utils.toSnakeCase({ ...(await getDefaultSearchParams(uiSettingsClient)), batchedReduceSize: 64, + keepOnCompletion: !!options.sessionId, // Always return an ID, even if the request completes quickly ...asyncOptions, ...request.params, }) From d9ff3916d5cb443b4ef3caefa30493760b73958c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 19 Nov 2020 10:52:25 -0600 Subject: [PATCH 11/16] [DOCS] Consolidates plugins (#83712) (#83813) --- docs/plugins/known-plugins.asciidoc | 74 ------------------------ docs/user/plugins.asciidoc | 89 ++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 83 deletions(-) delete mode 100644 docs/plugins/known-plugins.asciidoc diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc deleted file mode 100644 index 7b24de42d8e1c..0000000000000 --- a/docs/plugins/known-plugins.asciidoc +++ /dev/null @@ -1,74 +0,0 @@ -[[known-plugins]] -== Known Plugins - -[IMPORTANT] -.Plugin compatibility -============================================== -The Kibana plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. Kibana enforces that the installed plugins match the version of Kibana itself. Plugin developers will have to release a new version of their plugin for each new Kibana release as a result. -============================================== - -This list of plugins is not guaranteed to work on your version of Kibana. Instead, these are plugins that were known to work at some point with Kibana *5.x*. The Kibana installer will reject any plugins that haven't been published for your specific version of Kibana. These plugins are not evaluated or maintained by Elastic, so care should be taken before installing them into your environment. - -[float] -=== Apps -* https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface -* https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy -* https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation -* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. -* https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. -* https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API -* https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. -* https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules -* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights - -[float] -=== Timelion Extensions -* https://github.com/fermiumlabs/mathlion[mathlion] (fermiumlabs) - enables equation parsing and advanced math under Timelion - -[float] -=== Visualizations -* https://github.com/virusu/3D_kibana_charts_vis[3D Charts] (virusu) -* https://github.com/JuanCarniglia/area3d_vis[3D Graph] (JuanCarniglia) -* https://github.com/TrumanDu/bmap[Bmap](TrumanDu) - integrated echarts for map visualization -* https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano) -* https://github.com/aaronoah/kibana_calendar_vis[Calendar Visualization] (aaronoah) -* https://github.com/elo7/cohort[Cohort analysis] (elo7) -* https://github.com/DeanF/health_metric_vis[Colored Metric Visualization] (deanf) -* https://github.com/JuanCarniglia/dendrogram_vis[Dendrogram] (JuanCarniglia) -* https://github.com/dlumbrer/kbn_dotplot[Dotplot] (dlumbrer) -* https://github.com/AnnaGerber/kibana_dropdown[Dropdown] (AnnaGerber) -* https://github.com/fbaligand/kibana-enhanced-table[Enhanced Table] (fbaligand) -* https://github.com/nreese/enhanced_tilemap[Enhanced Tilemap] (nreese) -* https://github.com/ommsolutions/kibana_ext_metrics_vis[Extended Metric] (ommsolutions) -* https://github.com/flexmonster/pivot-kibana[Flexmonster Pivot Table & Charts] - a customizable pivot table component for advanced data analysis and reporting. -* https://github.com/outbrain/ob-kb-funnel[Funnel Visualization] (roybass) -* https://github.com/sbeyn/kibana-plugin-gauge-sg[Gauge] (sbeyn) -* https://github.com/clamarque/Kibana_health_metric_vis[Health Metric] (clamarque) -* https://github.com/tshoeb/Insight[Insight] (tshoeb) - Multidimensional data exploration -* https://github.com/sbeyn/kibana-plugin-line-sg[Line] (sbeyn) -* https://github.com/walterra/kibana-milestones-vis[Milestones] (walterra) -* https://github.com/varundbest/navigation[Navigation] (varundbest) -* https://github.com/dlumbrer/kbn_network[Network Plugin] (dlumbrer) -* https://github.com/amannocci/kibana-plugin-metric-percent[Percent] (amannocci) -* https://github.com/dlumbrer/kbn_polar[Polar] (dlumbrer) -* https://github.com/dlumbrer/kbn_radar[Radar] (dlumbrer) -* https://github.com/dlumbrer/kbn_searchtables[Search-Tables] (dlumbrer) -* https://github.com/Smeds/status_light_visualization[Status Light] (smeds) -* https://github.com/prelert/kibana-swimlane-vis[Swimlanes] (prelert) -* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn) -* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek) -* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity. -* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan) - -[float] -=== Other -* https://github.com/nreese/kibana-time-plugin[Time filter as a dashboard panel] Widget to view and edit the time range from within dashboards. - -* https://github.com/Webiks/kibana-API.git[Kibana-API] (webiks) Exposes an API with Kibana functionality. -Use it to create, edit and embed visualizations, and also to search inside an embedded dashboard. - -* 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: If you want your plugin to be added to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc index a96fe811dc84f..fa9e7d0c513b5 100644 --- a/docs/user/plugins.asciidoc +++ b/docs/user/plugins.asciidoc @@ -1,20 +1,90 @@ +[chapter] [[kibana-plugins]] -= Kibana plugins += {kib} plugins -[partintro] --- -Add-on functionality for {kib} is implemented with plug-in modules. You use the `bin/kibana-plugin` -command to manage these modules. +Implement add-on functionality for {kib} with plug-in modules. [IMPORTANT] .Plugin compatibility ============================================== -The {kib} plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. {kib} enforces that the installed plugins match the version of {kib} itself. Plugin developers will have to release a new version of their plugin for each new {kib} release as a result. +The {kib} plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. {kib} enforces that the installed plugins match the version of {kib}. +Plugin developers must release a new version of their plugin for each new {kib} release. ============================================== --- +[float] +[[known-plugins]] +== Known plugins + +The known plugins were tested for {kib} *5.x*, so we are unable to guarantee compatibility with your version of {kib}. The {kib} installer rejects any plugins that haven't been published for your specific version of {kib}. +We are unable to evaluate or maintain the known plugins, so care should be taken before installation. + +[float] +=== Apps +* https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface +* https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy +* https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation +* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. +* https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. +* https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API +* https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. +* https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules +* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights + +[float] +=== Timelion Extensions +* https://github.com/fermiumlabs/mathlion[mathlion] (fermiumlabs) - enables equation parsing and advanced math under Timelion + +[float] +=== Visualizations +* https://github.com/virusu/3D_kibana_charts_vis[3D Charts] (virusu) +* https://github.com/JuanCarniglia/area3d_vis[3D Graph] (JuanCarniglia) +* https://github.com/TrumanDu/bmap[Bmap](TrumanDu) - integrated echarts for map visualization +* https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano) +* https://github.com/aaronoah/kibana_calendar_vis[Calendar Visualization] (aaronoah) +* https://github.com/elo7/cohort[Cohort analysis] (elo7) +* https://github.com/DeanF/health_metric_vis[Colored Metric Visualization] (deanf) +* https://github.com/JuanCarniglia/dendrogram_vis[Dendrogram] (JuanCarniglia) +* https://github.com/dlumbrer/kbn_dotplot[Dotplot] (dlumbrer) +* https://github.com/AnnaGerber/kibana_dropdown[Dropdown] (AnnaGerber) +* https://github.com/fbaligand/kibana-enhanced-table[Enhanced Table] (fbaligand) +* https://github.com/nreese/enhanced_tilemap[Enhanced Tilemap] (nreese) +* https://github.com/ommsolutions/kibana_ext_metrics_vis[Extended Metric] (ommsolutions) +* https://github.com/flexmonster/pivot-kibana[Flexmonster Pivot Table & Charts] - a customizable pivot table component for advanced data analysis and reporting. +* https://github.com/outbrain/ob-kb-funnel[Funnel Visualization] (roybass) +* https://github.com/sbeyn/kibana-plugin-gauge-sg[Gauge] (sbeyn) +* https://github.com/clamarque/Kibana_health_metric_vis[Health Metric] (clamarque) +* https://github.com/tshoeb/Insight[Insight] (tshoeb) - Multidimensional data exploration +* https://github.com/sbeyn/kibana-plugin-line-sg[Line] (sbeyn) +* https://github.com/walterra/kibana-milestones-vis[Milestones] (walterra) +* https://github.com/varundbest/navigation[Navigation] (varundbest) +* https://github.com/dlumbrer/kbn_network[Network Plugin] (dlumbrer) +* https://github.com/amannocci/kibana-plugin-metric-percent[Percent] (amannocci) +* https://github.com/dlumbrer/kbn_polar[Polar] (dlumbrer) +* https://github.com/dlumbrer/kbn_radar[Radar] (dlumbrer) +* https://github.com/dlumbrer/kbn_searchtables[Search-Tables] (dlumbrer) +* https://github.com/Smeds/status_light_visualization[Status Light] (smeds) +* https://github.com/prelert/kibana-swimlane-vis[Swimlanes] (prelert) +* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn) +* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek) +* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity. +* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan) + +[float] +=== Other +* https://github.com/nreese/kibana-time-plugin[Time filter as a dashboard panel] Widget to view and edit the time range from within dashboards. + +* https://github.com/Webiks/kibana-API.git[Kibana-API] (webiks) Exposes an API with Kibana functionality. +Use it to create, edit and embed visualizations, and also to search inside an embedded dashboard. + +* 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]. + +[float] [[install-plugin]] == Install plugins @@ -60,6 +130,7 @@ You can specify the environment variable directly when installing plugins: [source,shell] $ http_proxy="http://proxy.local:4242" bin/kibana-plugin install +[float] [[update-remove-plugin]] == Update and remove plugins @@ -74,6 +145,7 @@ You can also remove a plugin manually by deleting the plugin's subdirectory unde NOTE: Removing a plugin will result in an "optimize" run which will delay the next start of {kib}. +[float] [[disable-plugin]] == Disable plugins @@ -88,6 +160,7 @@ NOTE: Disabling or enabling a plugin will result in an "optimize" run which will <1> You can find a plugin's plugin ID as the value of the `name` property in the plugin's `package.json` file. +[float] [[configure-plugin-manager]] == Configure the plugin manager @@ -125,5 +198,3 @@ you must specify the path to that configuration file each time you use the `bin/ 64:: Unknown command or incorrect option parameter 74:: I/O error 70:: Other error - -include::{kib-repo-dir}/plugins/known-plugins.asciidoc[] From c421491bcef79aff5a1b88fe35b550d91497c9eb Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 19 Nov 2020 16:52:26 +0000 Subject: [PATCH 12/16] skip flaky suite (#79389) --- .../cypress/integration/timeline_creation.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts index 9f61d11b7ac0f..8ce60450671b9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts @@ -45,7 +45,8 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -describe('Timelines', () => { +// FLAKY: https://github.com/elastic/kibana/issues/79389 +describe.skip('Timelines', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); From db21b380b15fa7cb8a07e01851900e72e2b89d5e Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 19 Nov 2020 11:06:24 -0600 Subject: [PATCH 13/16] [DOCS] Reallocates limitations to point-of-use (#79582) (#83825) * [DOCS] Reallocates limitations to point-of-use * KQL changes * Removed limitations file * Review comments --- docs/discover/search.asciidoc | 3 +++ docs/index.asciidoc | 2 -- docs/limitations.asciidoc | 20 -------------------- docs/management/watcher-ui/index.asciidoc | 2 ++ docs/user/ml/index.asciidoc | 2 ++ docs/user/reporting/index.asciidoc | 2 ++ docs/user/security/index.asciidoc | 2 ++ 7 files changed, 11 insertions(+), 22 deletions(-) diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 3720a5b457d84..75c6fddb484ac 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -74,6 +74,9 @@ status codes, you could enter `status:[400 TO 499]`. codes and have an extension of `php` or `html`, you could enter `status:[400 TO 499] AND (extension:php OR extension:html)`. +IMPORTANT: When you use the Lucene Query Syntax in the *KQL* search bar, {kib} is unable to search on nested objects and perform aggregations across fields that contain nested objects. +Using `include_in_parent` or `copy_to` as a workaround can cause {kib} to fail. + For more detailed information about the Lucene query syntax, see the {ref}/query-dsl-query-string-query.html#query-string-syntax[Query String Query] docs. diff --git a/docs/index.asciidoc b/docs/index.asciidoc index f4d2b7d066f12..0147a66704de8 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -21,8 +21,6 @@ include::user/index.asciidoc[] include::accessibility.asciidoc[] -include::limitations.asciidoc[] - include::migration.asciidoc[] include::CHANGELOG.asciidoc[] diff --git a/docs/limitations.asciidoc b/docs/limitations.asciidoc index 30a716641cc5d..97d3bd9d4f73c 100644 --- a/docs/limitations.asciidoc +++ b/docs/limitations.asciidoc @@ -4,12 +4,6 @@ Following are the known limitations in {kib}. -[float] -=== Exporting data - -Exporting a data table or saved search from a dashboard or visualization report -has known limitations. The PDF report only includes the data visible on the screen. - [float] === Nested objects @@ -22,17 +16,3 @@ the query bar. Using `include_in_parent` or `copy_to` as a workaround is not supported and may stop functioning in future releases. ============================================== -[float] -=== Graph - -Graph has limited support for multiple indices. -Go to <> for details. - -[float] -=== Other limitations - -These {stack} features have limitations that affect {kib}: - -* {ref}/watcher-limitations.html[Alerting] -* {ml-docs}/ml-limitations.html[Machine learning] -* {ref}/security-limitations.html[Security] diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index aded7a45022db..0bc6365918866 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -27,6 +27,8 @@ threshold watch, take a look at the different watcher actions. If you are creating an advanced watch, you should be familiar with the parts of a watch—input, schedule, condition, and actions. +NOTE: There are limitations in *Watcher* that affect {kib}. For information, refer to {ref}/watcher-limitations.html[Alerting]. + [float] [[watcher-security]] === Watcher security diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index 8255585aae411..fa15e0652e2ab 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -26,6 +26,8 @@ If {stack-security-features} are enabled, users must have the necessary privileges to use {ml-features}. Refer to {ml-docs}/setup.html#setup-privileges[Set up {ml-features}]. +NOTE: There are limitations in {ml-features} that affect {kib}. For more information, refer to {ml-docs}/ml-limitations.html[Machine learning]. + -- [[xpack-ml-anomalies]] diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index cd93389bb5fde..224973d3c840c 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -55,6 +55,8 @@ click the share icon image:user/reporting/images/canvas-share-button.png["Canvas + A notification appears when the report is complete. +NOTE: When you export a data table or saved search from a dashboard report, the PDF includes only the visible data. + [float] [[reporting-layout-sizing]] == Layout and sizing diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index 18ace452ce00c..f84e9de87c734 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -10,6 +10,8 @@ auditing. For more information, see {ref}/secure-cluster.html[Secure a cluster] and <>. +NOTE: There are security limitations that affect {kib}. For more information, refer to {ref}/security-limitations.html[Security]. + [float] === Required permissions From 972e7dafb136951a03dbdc2decc830b81d18529e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 19 Nov 2020 17:11:29 +0000 Subject: [PATCH 14/16] [Task Manager] Ensures retries are inferred from the schedule of recurring tasks (#83682) (#83800) This addresses a bug in Task Manager in the task timeout behaviour. When a recurring task's `retryAt` field is set (which happens at task run), it is currently scheduled to the task definition's `timeout` value, but the original intention was for these tasks to retry on their next scheduled run (originally identified as part of https://github.com/elastic/kibana/issues/39349). In this PR we ensure recurring task retries are scheduled according to their recurring schedule, rather than the default `timeout` of the task type. --- .../task_manager/server/lib/intervals.test.ts | 39 ++++++++ .../task_manager/server/lib/intervals.ts | 12 ++- .../server/task_running/task_runner.test.ts | 96 +++++++++++++++++++ .../server/task_running/task_runner.ts | 21 ++-- .../sample_task_plugin/server/plugin.ts | 11 +++ .../task_manager/task_management.ts | 22 +++++ 6 files changed, 190 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/task_manager/server/lib/intervals.test.ts b/x-pack/plugins/task_manager/server/lib/intervals.test.ts index 147e41e1a9d60..efef05843cb40 100644 --- a/x-pack/plugins/task_manager/server/lib/intervals.test.ts +++ b/x-pack/plugins/task_manager/server/lib/intervals.test.ts @@ -14,6 +14,7 @@ import { secondsFromNow, secondsFromDate, asInterval, + maxIntervalFromDate, } from './intervals'; let fakeTimer: sinon.SinonFakeTimers; @@ -159,6 +160,44 @@ describe('taskIntervals', () => { }); }); + describe('maxIntervalFromDate', () => { + test('it handles a single interval', () => { + const mins = _.random(1, 100); + const now = new Date(); + const expected = now.getTime() + mins * 60 * 1000; + expect(maxIntervalFromDate(now, `${mins}m`)!.getTime()).toEqual(expected); + }); + + test('it handles multiple intervals', () => { + const mins = _.random(1, 100); + const maxMins = mins + _.random(1, 100); + const now = new Date(); + const expected = now.getTime() + maxMins * 60 * 1000; + expect(maxIntervalFromDate(now, `${mins}m`, `${maxMins}m`)!.getTime()).toEqual(expected); + }); + + test('it handles multiple mixed type intervals', () => { + const mins = _.random(1, 100); + const seconds = _.random(1, 100); + const maxSeconds = Math.max(mins * 60, seconds) + _.random(1, 100); + const now = new Date(); + const expected = now.getTime() + maxSeconds * 1000; + expect( + maxIntervalFromDate(now, `${mins}m`, `${maxSeconds}s`, `${seconds}s`)!.getTime() + ).toEqual(expected); + }); + + test('it handles undefined intervals', () => { + const mins = _.random(1, 100); + const maxMins = mins + _.random(1, 100); + const now = new Date(); + const expected = now.getTime() + maxMins * 60 * 1000; + expect(maxIntervalFromDate(now, `${mins}m`, undefined, `${maxMins}m`)!.getTime()).toEqual( + expected + ); + }); + }); + describe('intervalFromDate', () => { test('it returns the given date plus n minutes', () => { const originalDate = new Date(2019, 1, 1); diff --git a/x-pack/plugins/task_manager/server/lib/intervals.ts b/x-pack/plugins/task_manager/server/lib/intervals.ts index 94537277123ee..da04dffa4b5d1 100644 --- a/x-pack/plugins/task_manager/server/lib/intervals.ts +++ b/x-pack/plugins/task_manager/server/lib/intervals.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { memoize } from 'lodash'; +import { isString, memoize } from 'lodash'; export enum IntervalCadence { Minute = 'm', @@ -57,6 +57,16 @@ export function intervalFromDate(date: Date, interval?: string): Date | undefine return secondsFromDate(date, parseIntervalAsSecond(interval)); } +export function maxIntervalFromDate( + date: Date, + ...intervals: Array +): Date | undefined { + const maxSeconds = Math.max(...intervals.filter(isString).map(parseIntervalAsSecond)); + if (!isNaN(maxSeconds)) { + return secondsFromDate(date, maxSeconds); + } +} + /** * Returns a date that is secs seconds from now. * diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts index f5e2d3d96bc42..3777d89ce63dd 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts @@ -393,6 +393,102 @@ describe('TaskManagerRunner', () => { ); }); + test('calculates retryAt by schedule when running a recurring task', async () => { + const intervalMinutes = 10; + const id = _.random(1, 20).toString(); + const initialAttempts = _.random(0, 2); + const { runner, store } = testOpts({ + instance: { + id, + attempts: initialAttempts, + schedule: { + interval: `${intervalMinutes}m`, + }, + }, + definitions: { + bar: { + title: 'Bar!', + createTaskRunner: () => ({ + run: async () => undefined, + }), + }, + }, + }); + + await runner.markTaskAsRunning(); + + sinon.assert.calledOnce(store.update); + const instance = store.update.args[0][0]; + + expect(instance.retryAt.getTime()).toEqual( + instance.startedAt.getTime() + intervalMinutes * 60 * 1000 + ); + }); + + test('calculates retryAt by default timout when it exceeds the schedule of a recurring task', async () => { + const intervalSeconds = 20; + const id = _.random(1, 20).toString(); + const initialAttempts = _.random(0, 2); + const { runner, store } = testOpts({ + instance: { + id, + attempts: initialAttempts, + schedule: { + interval: `${intervalSeconds}s`, + }, + }, + definitions: { + bar: { + title: 'Bar!', + createTaskRunner: () => ({ + run: async () => undefined, + }), + }, + }, + }); + + await runner.markTaskAsRunning(); + + sinon.assert.calledOnce(store.update); + const instance = store.update.args[0][0]; + + expect(instance.retryAt.getTime()).toEqual(instance.startedAt.getTime() + 5 * 60 * 1000); + }); + + test('calculates retryAt by timeout if it exceeds the schedule when running a recurring task', async () => { + const timeoutMinutes = 1; + const intervalSeconds = 20; + const id = _.random(1, 20).toString(); + const initialAttempts = _.random(0, 2); + const { runner, store } = testOpts({ + instance: { + id, + attempts: initialAttempts, + schedule: { + interval: `${intervalSeconds}s`, + }, + }, + definitions: { + bar: { + title: 'Bar!', + timeout: `${timeoutMinutes}m`, + createTaskRunner: () => ({ + run: async () => undefined, + }), + }, + }, + }); + + await runner.markTaskAsRunning(); + + sinon.assert.calledOnce(store.update); + const instance = store.update.args[0][0]; + + expect(instance.retryAt.getTime()).toEqual( + instance.startedAt.getTime() + timeoutMinutes * 60 * 1000 + ); + }); + test('uses getRetry function (returning date) on error when defined', async () => { const initialAttempts = _.random(1, 3); const nextRetry = new Date(Date.now() + _.random(15, 100) * 1000); diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index fb7a28c8f402c..23d21d205ec26 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -26,7 +26,7 @@ import { startTaskTimer, TaskTiming, } from '../task_events'; -import { intervalFromDate, intervalFromNow } from '../lib/intervals'; +import { intervalFromDate, maxIntervalFromDate } from '../lib/intervals'; import { CancelFunction, CancellableTask, @@ -259,15 +259,16 @@ export class TaskManagerRunner implements TaskRunner { status: TaskStatus.Running, startedAt: now, attempts, - retryAt: this.instance.schedule - ? intervalFromNow(this.definition.timeout)! - : this.getRetryDelay({ - attempts, - // Fake an error. This allows retry logic when tasks keep timing out - // and lets us set a proper "retryAt" value each time. - error: new Error('Task timeout'), - addDuration: this.definition.timeout, - }) ?? null, + retryAt: + (this.instance.schedule + ? maxIntervalFromDate(now, this.instance.schedule!.interval, this.definition.timeout) + : this.getRetryDelay({ + attempts, + // Fake an error. This allows retry logic when tasks keep timing out + // and lets us set a proper "retryAt" value each time. + error: new Error('Task timeout'), + addDuration: this.definition.timeout, + })) ?? null, }); const timeUntilClaimExpiresAfterUpdate = howManyMsUntilOwnershipClaimExpires( diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts index b5d2c98d8cbcd..0326adb90775a 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -115,6 +115,17 @@ export class SampleTaskManagerFixturePlugin }, }), }, + sampleRecurringTaskWhichHangs: { + title: 'Sample Recurring Task that Hangs for a minute', + description: 'A sample task that Hangs for a minute on each run.', + maxAttempts: 3, + timeout: '60s', + createTaskRunner: () => ({ + async run() { + return await new Promise((resolve) => {}); + }, + }), + }, sampleOneTimeTaskTimingOut: { title: 'Sample One-Time Task that Times Out', description: 'A sample task that times out each run.', diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts index f34cb7594d288..7f4585fad4729 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts @@ -260,6 +260,28 @@ export default function ({ getService }: FtrProviderContext) { }); }); + it('should schedule the retry of recurring tasks to run at the next schedule when they time out', async () => { + const intervalInMinutes = 30; + const intervalInMilliseconds = intervalInMinutes * 60 * 1000; + const task = await scheduleTask({ + taskType: 'sampleRecurringTaskWhichHangs', + schedule: { interval: `${intervalInMinutes}m` }, + params: {}, + }); + + await retry.try(async () => { + const [scheduledTask] = (await currentTasks()).docs; + expect(scheduledTask.id).to.eql(task.id); + const retryAt = Date.parse(scheduledTask.retryAt!); + expect(isNaN(retryAt)).to.be(false); + + const buffer = 10000; // 10 second buffer + const retryDelay = retryAt - Date.parse(task.runAt); + expect(retryDelay).to.be.greaterThan(intervalInMilliseconds - buffer); + expect(retryDelay).to.be.lessThan(intervalInMilliseconds + buffer); + }); + }); + it('should reschedule if task returns runAt', async () => { const nextRunMilliseconds = _.random(60000, 200000); const count = _.random(1, 20); From d337ed28c36bddb84e57e4961cfa259c949cb0f9 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 19 Nov 2020 12:47:53 -0500 Subject: [PATCH 15/16] [Fleet] Rename ingestManager plugin ID fleet (#83200) (#83794) --- docs/developer/plugin-list.asciidoc | 8 +-- packages/kbn-optimizer/limits.yml | 2 +- .../setup-custom-kibana-user-role.ts | 4 +- x-pack/plugins/fleet/README.md | 4 +- .../plugins/fleet/common/constants/plugin.ts | 2 +- .../common/services/decode_cloud_id.test.ts | 2 +- .../services/is_agent_upgradeable.test.ts | 2 +- .../services/is_diff_path_protocol.test.ts | 2 +- .../services/is_valid_namespace.test.ts | 2 +- .../package_policies_to_agent_inputs.test.ts | 2 +- .../package_to_package_policy.test.ts | 2 +- x-pack/plugins/fleet/common/types/index.ts | 2 +- x-pack/plugins/fleet/kibana.json | 2 +- x-pack/plugins/fleet/package.json | 4 +- .../fleet/constants/page_paths.ts | 2 +- .../fleet/hooks/use_capabilities.ts | 2 +- .../applications/fleet/hooks/use_config.ts | 4 +- .../applications/fleet/hooks/use_deps.ts | 6 +- .../fleet/public/applications/fleet/index.tsx | 20 +++---- .../has_invalid_but_required_var.test.ts | 2 +- .../services/is_advanced_var.test.ts | 2 +- .../services/validate_package_policy.test.ts | 4 +- .../components/agent_unenroll_modal/index.tsx | 2 +- .../fleet/sections/agents/index.tsx | 6 +- x-pack/plugins/fleet/public/index.ts | 6 +- x-pack/plugins/fleet/public/plugin.ts | 59 +++++++++++-------- .../server/collectors/config_collectors.ts | 4 +- .../fleet/server/collectors/register.ts | 4 +- x-pack/plugins/fleet/server/index.ts | 13 ++-- x-pack/plugins/fleet/server/mocks.ts | 4 +- x-pack/plugins/fleet/server/plugin.ts | 54 ++++++++--------- .../fleet/server/routes/agent/index.ts | 4 +- .../server/routes/limited_concurrency.test.ts | 8 +-- .../server/routes/limited_concurrency.ts | 4 +- .../server/routes/setup/handlers.test.ts | 10 ++-- .../fleet/server/routes/setup/handlers.ts | 2 +- .../fleet/server/routes/setup/index.ts | 16 ++--- .../fleet/server/services/app_context.ts | 16 ++--- .../plugins/fleet/server/services/config.ts | 10 ++-- .../home/data_streams_tab.test.ts | 6 +- x-pack/plugins/index_management/kibana.json | 14 +---- .../public/application/app_context.tsx | 4 +- .../application/mount_management_section.ts | 6 +- .../data_stream_list/data_stream_list.tsx | 8 +-- .../plugins/index_management/public/plugin.ts | 4 +- .../plugins/index_management/public/types.ts | 4 +- .../index.tsx | 16 ++--- .../public/pages/landing/index.tsx | 4 +- x-pack/plugins/security_solution/kibana.json | 4 +- .../public/app/home/setup.tsx | 10 ++-- .../__snapshots__/link_to_app.test.tsx.snap | 10 ++-- .../components/endpoint/link_to_app.test.tsx | 28 ++++----- .../common/hooks/endpoint/ingest_enabled.ts | 12 ++-- .../use_navigate_to_app_event_handler.ts | 2 +- .../mock/endpoint/app_context_render.tsx | 2 +- .../mock/endpoint/app_root_provider.tsx | 2 +- .../mock/endpoint/dependencies_start_mock.ts | 6 +- .../public/common/store/types.ts | 4 +- .../pages/endpoint_hosts/view/hooks.ts | 12 ++-- .../pages/endpoint_hosts/view/index.test.tsx | 18 +++--- .../pages/endpoint_hosts/view/index.tsx | 16 ++--- .../endpoint_policy_edit_extension.tsx | 14 ++--- .../pages/policy/view/policy_list.tsx | 6 +- .../security_solution/public/plugin.tsx | 4 +- .../plugins/security_solution/public/types.ts | 4 +- .../endpoint/endpoint_app_context_services.ts | 6 +- .../server/endpoint/mocks.ts | 10 ++-- .../endpoint/routes/metadata/metadata.test.ts | 2 +- .../routes/metadata/metadata_v1.test.ts | 2 +- .../security_solution/server/plugin.ts | 14 ++--- .../translations/translations/ja-JP.json | 10 ---- .../translations/translations/zh-CN.json | 10 ---- .../apis/features/features/features.ts | 2 +- .../apis/security/privileges.ts | 2 +- .../apis/security/privileges_basic.ts | 2 +- .../apis/agents/delete.ts | 4 +- .../fleet_api_integration/apis/agents/list.ts | 4 +- ...gest_manager_create_package_policy_page.ts | 2 +- 78 files changed, 277 insertions(+), 317 deletions(-) rename x-pack/plugins/observability/public/components/app/{ingest_manager_panel => fleet_panel}/index.tsx (74%) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b8b3d6387d093..a4ba0cb3e1056 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -373,6 +373,10 @@ and actions. |Backend and core front-end react-components for GeoJson file upload. Only supports the Maps plugin. +|{kib-repo}blob/{branch}/x-pack/plugins/fleet/README.md[fleet] +|Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.fleet.agents.tlsCheckDisabled=false) + + |{kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] |The GlobalSearch plugin provides an easy way to search for various objects, such as applications or dashboards from the Kibana instance, from both server and client-side plugins @@ -409,10 +413,6 @@ Index Management by running this series of requests in Console: the infrastructure monitoring use-case within Kibana. -|{kib-repo}blob/{branch}/x-pack/plugins/fleet/README.md[ingestManager] -|Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.fleet.agents.tlsCheckDisabled=false) - - |{kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] |The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c12cf45d0c321..3e9c5bdf964d2 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -36,7 +36,7 @@ pageLoadAssetSize: indexManagement: 662506 indexPatternManagement: 154366 infra: 197873 - ingestManager: 415829 + fleet: 415829 ingestPipelines: 58003 inputControlVis: 172819 inspector: 148999 diff --git a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index cf17c9dbbf2e3..11383f23964f8 100644 --- a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -148,7 +148,7 @@ async function init() { indexPatterns: ['read'], savedObjectsManagement: ['read'], stackAlerts: ['read'], - ingestManager: ['read'], + fleet: ['read'], actions: ['read'], }, }, @@ -181,7 +181,7 @@ async function init() { indexPatterns: ['all'], savedObjectsManagement: ['all'], stackAlerts: ['all'], - ingestManager: ['all'], + fleet: ['all'], actions: ['all'], }, }, diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index 614e1aba2ab86..b1f52dbed9cfb 100644 --- a/x-pack/plugins/fleet/README.md +++ b/x-pack/plugins/fleet/README.md @@ -1,4 +1,4 @@ -# Ingest Manager +# Fleet ## Plugin @@ -46,6 +46,8 @@ One common development workflow is: This plugin follows the `common`, `server`, `public` structure from the [Architecture Style Guide ](https://github.com/elastic/kibana/blob/master/style_guides/architecture_style_guide.md#file-and-folder-structure). We also follow the pattern of developing feature branches under your personal fork of Kibana. +Note: The plugin was previously named Ingest Manager it's possible that some variables are still named with that old plugin name. + ### Tests #### API integration tests diff --git a/x-pack/plugins/fleet/common/constants/plugin.ts b/x-pack/plugins/fleet/common/constants/plugin.ts index c2390bb433953..e7262761c4dcf 100644 --- a/x-pack/plugins/fleet/common/constants/plugin.ts +++ b/x-pack/plugins/fleet/common/constants/plugin.ts @@ -3,4 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export const PLUGIN_ID = 'ingestManager'; +export const PLUGIN_ID = 'fleet'; diff --git a/x-pack/plugins/fleet/common/services/decode_cloud_id.test.ts b/x-pack/plugins/fleet/common/services/decode_cloud_id.test.ts index dcec54f47440a..8a5fee3ee2172 100644 --- a/x-pack/plugins/fleet/common/services/decode_cloud_id.test.ts +++ b/x-pack/plugins/fleet/common/services/decode_cloud_id.test.ts @@ -5,7 +5,7 @@ */ import { decodeCloudId } from './decode_cloud_id'; -describe('Ingest Manager - decodeCloudId', () => { +describe('Fleet - decodeCloudId', () => { it('parses various CloudID formats', () => { const tests = [ { diff --git a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts index dc61f4898478d..1a9e5f09f6670 100644 --- a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts +++ b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts @@ -94,7 +94,7 @@ const getAgent = ({ } return agent; }; -describe('Ingest Manager - isAgentUpgradeable', () => { +describe('Fleet - isAgentUpgradeable', () => { it('returns false if agent reports not upgradeable with agent version < kibana version', () => { expect(isAgentUpgradeable(getAgent({ version: '7.9.0' }), '8.0.0')).toBe(false); }); diff --git a/x-pack/plugins/fleet/common/services/is_diff_path_protocol.test.ts b/x-pack/plugins/fleet/common/services/is_diff_path_protocol.test.ts index c488d552d7676..6c49bba49a582 100644 --- a/x-pack/plugins/fleet/common/services/is_diff_path_protocol.test.ts +++ b/x-pack/plugins/fleet/common/services/is_diff_path_protocol.test.ts @@ -5,7 +5,7 @@ */ import { isDiffPathProtocol } from './is_diff_path_protocol'; -describe('Ingest Manager - isDiffPathProtocol', () => { +describe('Fleet - isDiffPathProtocol', () => { it('returns true for different paths', () => { expect( isDiffPathProtocol([ diff --git a/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts b/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts index 3ed9e3a087a92..8d60c4aa61dca 100644 --- a/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts +++ b/x-pack/plugins/fleet/common/services/is_valid_namespace.test.ts @@ -5,7 +5,7 @@ */ import { isValidNamespace } from './is_valid_namespace'; -describe('Ingest Manager - isValidNamespace', () => { +describe('Fleet - isValidNamespace', () => { it('returns true for valid namespaces', () => { expect(isValidNamespace('default').valid).toBe(true); expect(isValidNamespace('namespace-with-dash').valid).toBe(true); diff --git a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts index 1df06df1de275..f721afb639141 100644 --- a/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts +++ b/x-pack/plugins/fleet/common/services/package_policies_to_agent_inputs.test.ts @@ -6,7 +6,7 @@ import { PackagePolicy, PackagePolicyInput } from '../types'; import { storedPackagePoliciesToAgentInputs } from './package_policies_to_agent_inputs'; -describe('Ingest Manager - storedPackagePoliciesToAgentInputs', () => { +describe('Fleet - storedPackagePoliciesToAgentInputs', () => { const mockPackagePolicy: PackagePolicy = { id: 'some-uuid', name: 'mock-package-policy', diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index e81207300a5f3..ae4de55ffa9a8 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -7,7 +7,7 @@ import { installationStatuses } from '../constants'; import { PackageInfo } from '../types'; import { packageToPackagePolicy, packageToPackagePolicyInputs } from './package_to_package_policy'; -describe('Ingest Manager - packageToPackagePolicy', () => { +describe('Fleet - packageToPackagePolicy', () => { const mockPackage: PackageInfo = { name: 'mock-package', title: 'Mock package', diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index ba76194b1d9b9..e0827ef7cf40f 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -6,7 +6,7 @@ export * from './models'; export * from './rest_spec'; -export interface IngestManagerConfigType { +export interface FleetConfigType { enabled: boolean; registryUrl?: string; registryProxyUrl?: string; diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index 5ea6d21e1282e..81b56682b47e1 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -1,5 +1,5 @@ { - "id": "ingestManager", + "id": "fleet", "version": "kibana", "server": true, "ui": true, diff --git a/x-pack/plugins/fleet/package.json b/x-pack/plugins/fleet/package.json index d2bb7a1621d9f..e374dabb82458 100644 --- a/x-pack/plugins/fleet/package.json +++ b/x-pack/plugins/fleet/package.json @@ -1,7 +1,7 @@ { "author": "Elastic", - "name": "ingest-manager", + "name": "fleet", "version": "8.0.0", "private": true, "license": "Elastic-License" -} \ No newline at end of file +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index 1273fb9b86ca9..9963753651671 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -31,7 +31,7 @@ export interface DynamicPagePathValues { [key: string]: string; } -export const BASE_PATH = '/app/ingestManager'; +export const BASE_PATH = '/app/fleet'; // If routing paths are changed here, please also check to see if // `pagePathGetters()`, below, needs any modifications diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts index 0a16c4a62a7d1..d8535183bb84e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_capabilities.ts @@ -8,5 +8,5 @@ import { useCore } from './'; export function useCapabilities() { const core = useCore(); - return core.application.capabilities.ingestManager; + return core.application.capabilities.fleet; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_config.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_config.ts index d3f27a180cfd0..e12265d162423 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_config.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_config.ts @@ -5,9 +5,9 @@ */ import React, { useContext } from 'react'; -import { IngestManagerConfigType } from '../../../plugin'; +import { FleetConfigType } from '../../../plugin'; -export const ConfigContext = React.createContext(null); +export const ConfigContext = React.createContext(null); export function useConfig() { const config = useContext(ConfigContext); diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_deps.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_deps.ts index 25e4ee8fca43c..bf8f33297882e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_deps.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_deps.ts @@ -5,11 +5,11 @@ */ import React, { useContext } from 'react'; -import { IngestManagerSetupDeps, IngestManagerStartDeps } from '../../../plugin'; +import { FleetSetupDeps, FleetStartDeps } from '../../../plugin'; export const DepsContext = React.createContext<{ - setup: IngestManagerSetupDeps; - start: IngestManagerStartDeps; + setup: FleetSetupDeps; + start: FleetStartDeps; } | null>(null); export function useSetupDeps() { diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index d4e652ad95831..51c897b3661cc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -14,11 +14,7 @@ import { EuiErrorBoundary, EuiPanel, EuiEmptyPrompt, EuiCode } from '@elastic/eu import { CoreStart, AppMountParameters } from 'src/core/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../xpack_legacy/common'; -import { - IngestManagerSetupDeps, - IngestManagerConfigType, - IngestManagerStartDeps, -} from '../../plugin'; +import { FleetSetupDeps, FleetConfigType, FleetStartDeps } from '../../plugin'; import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; @@ -241,9 +237,9 @@ const IngestManagerApp = ({ }: { basepath: string; coreStart: CoreStart; - setupDeps: IngestManagerSetupDeps; - startDeps: IngestManagerStartDeps; - config: IngestManagerConfigType; + setupDeps: FleetSetupDeps; + startDeps: FleetStartDeps; + config: FleetConfigType; history: AppMountParameters['history']; kibanaVersion: string; extensions: UIExtensionsStorage; @@ -271,9 +267,9 @@ const IngestManagerApp = ({ export function renderApp( coreStart: CoreStart, { element, appBasePath, history }: AppMountParameters, - setupDeps: IngestManagerSetupDeps, - startDeps: IngestManagerStartDeps, - config: IngestManagerConfigType, + setupDeps: FleetSetupDeps, + startDeps: FleetStartDeps, + config: FleetConfigType, kibanaVersion: string, extensions: UIExtensionsStorage ) { @@ -296,7 +292,7 @@ export function renderApp( }; } -export const teardownIngestManager = (coreStart: CoreStart) => { +export const teardownFleet = (coreStart: CoreStart) => { coreStart.chrome.docTitle.reset(); coreStart.chrome.setBreadcrumbs([]); licenseService.stop(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.test.ts index 679ae4b1456d6..05eb40fecb1c8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.test.ts @@ -5,7 +5,7 @@ */ import { hasInvalidButRequiredVar } from './has_invalid_but_required_var'; -describe('Ingest Manager - hasInvalidButRequiredVar', () => { +describe('Fleet - hasInvalidButRequiredVar', () => { it('returns true for invalid & required vars', () => { expect( hasInvalidButRequiredVar( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/is_advanced_var.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/is_advanced_var.test.ts index 67796d69863fa..d58068683086e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/is_advanced_var.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/is_advanced_var.test.ts @@ -5,7 +5,7 @@ */ import { isAdvancedVar } from './is_advanced_var'; -describe('Ingest Manager - isAdvancedVar', () => { +describe('Fleet - isAdvancedVar', () => { it('returns true for vars that should be show under advanced options', () => { expect( isAdvancedVar({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts index 8d46fed1ff14e..e3e29134d405e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts @@ -7,7 +7,7 @@ import { installationStatuses } from '../../../../../../../common/constants'; import { PackageInfo, NewPackagePolicy, RegistryPolicyTemplate } from '../../../../types'; import { validatePackagePolicy, validationHasErrors } from './validate_package_policy'; -describe('Ingest Manager - validatePackagePolicy()', () => { +describe('Fleet - validatePackagePolicy()', () => { const mockPackage = ({ name: 'mock-package', title: 'Mock package', @@ -496,7 +496,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => { }); }); -describe('Ingest Manager - validationHasErrors()', () => { +describe('Fleet - validationHasErrors()', () => { it('returns true for stream validation results with errors', () => { expect( validationHasErrors({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx index 74f2303c70c0a..1b3935a86f65c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx @@ -144,7 +144,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ }} > { useBreadcrumbs('fleet'); - const core = useCore(); const { agents } = useConfig(); + const capabilities = useCapabilities(); const fleetStatus = useFleetStatus(); @@ -35,7 +35,7 @@ export const FleetApp: React.FunctionComponent = () => { /> ); } - if (!core.application.capabilities.ingestManager.read) { + if (!capabilities.read) { return ; } diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index 1de001a6fc69e..be53af77f4b46 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { PluginInitializerContext } from 'src/core/public'; -import { IngestManagerPlugin } from './plugin'; +import { FleetPlugin } from './plugin'; -export { IngestManagerSetup, IngestManagerStart } from './plugin'; +export { FleetSetup, FleetStart } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { - return new IngestManagerPlugin(initializerContext); + return new FleetPlugin(initializerContext); }; export type { NewPackagePolicy } from './applications/fleet/types'; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 377ba770b5ca2..7e523b3fa594a 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -11,7 +11,7 @@ import { CoreStart, } from 'src/core/public'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '../../../../src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { HomePublicPluginSetup, @@ -21,7 +21,7 @@ import { LicensingPluginSetup } from '../../licensing/public'; import { PLUGIN_ID, CheckPermissionsResponse, PostIngestSetupResponse } from '../common'; import { BASE_PATH } from './applications/fleet/constants'; -import { IngestManagerConfigType } from '../common/types'; +import { FleetConfigType } from '../common/types'; import { setupRouteService, appRoutesService } from '../common'; import { licenseService } from './applications/fleet/hooks/use_license'; import { setHttpClient } from './applications/fleet/hooks/use_request/use_request'; @@ -33,44 +33,42 @@ import { import { createExtensionRegistrationCallback } from './applications/fleet/services/ui_extensions'; import { UIExtensionRegistrationCallback, UIExtensionsStorage } from './applications/fleet/types'; -export { IngestManagerConfigType } from '../common/types'; +export { FleetConfigType } from '../common/types'; -// We need to provide an object instead of void so that dependent plugins know when Ingest Manager +// We need to provide an object instead of void so that dependent plugins know when Fleet // is disabled. // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IngestManagerSetup {} +export interface FleetSetup {} /** - * Describes public IngestManager plugin contract returned at the `start` stage. + * Describes public Fleet plugin contract returned at the `start` stage. */ -export interface IngestManagerStart { +export interface FleetStart { registerExtension: UIExtensionRegistrationCallback; isInitialized: () => Promise; } -export interface IngestManagerSetupDeps { +export interface FleetSetupDeps { licensing: LicensingPluginSetup; data: DataPublicPluginSetup; home?: HomePublicPluginSetup; } -export interface IngestManagerStartDeps { +export interface FleetStartDeps { data: DataPublicPluginStart; } -export class IngestManagerPlugin - implements - Plugin { - private config: IngestManagerConfigType; +export class FleetPlugin implements Plugin { + private config: FleetConfigType; private kibanaVersion: string; private extensions: UIExtensionsStorage = {}; constructor(private readonly initializerContext: PluginInitializerContext) { - this.config = this.initializerContext.config.get(); + this.config = this.initializerContext.config.get(); this.kibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + public setup(core: CoreSetup, deps: FleetSetupDeps) { const config = this.config; const kibanaVersion = this.kibanaVersion; const extensions = this.extensions; @@ -81,7 +79,7 @@ export class IngestManagerPlugin // Set up license service licenseService.start(deps.licensing.license$); - // Register main Ingest Manager app + // Register main Fleet app core.application.register({ id: PLUGIN_ID, category: DEFAULT_APP_CATEGORIES.management, @@ -91,10 +89,10 @@ export class IngestManagerPlugin async mount(params: AppMountParameters) { const [coreStart, startDeps] = (await core.getStartServices()) as [ CoreStart, - IngestManagerStartDeps, - IngestManagerStart + FleetStartDeps, + FleetStart ]; - const { renderApp, teardownIngestManager } = await import('./applications/fleet/'); + const { renderApp, teardownFleet } = await import('./applications/fleet/'); const unmount = renderApp( coreStart, params, @@ -107,11 +105,26 @@ export class IngestManagerPlugin return () => { unmount(); - teardownIngestManager(coreStart); + teardownFleet(coreStart); }; }, }); + // BWC < 7.11 redirect /app/ingestManager to /app/fleet + core.application.register({ + id: 'ingestManager', + category: DEFAULT_APP_CATEGORIES.management, + navLinkStatus: AppNavLinkStatus.hidden, + title: i18n.translate('xpack.fleet.oldAppTitle', { defaultMessage: 'Ingest Manager' }), + async mount(params: AppMountParameters) { + const [coreStart] = await core.getStartServices(); + coreStart.application.navigateToApp('fleet', { + path: params.history.location.hash, + }); + return () => {}; + }, + }); + // Register components for home/add data integration if (deps.home) { deps.home.tutorials.registerDirectoryNotice(PLUGIN_ID, TutorialDirectoryNotice); @@ -119,7 +132,7 @@ export class IngestManagerPlugin deps.home.tutorials.registerModuleNotice(PLUGIN_ID, TutorialModuleNotice); deps.home.featureCatalogue.register({ - id: 'ingestManager', + id: 'fleet', title: i18n.translate('xpack.fleet.featureCatalogueTitle', { defaultMessage: 'Add Elastic Agent', }), @@ -137,8 +150,8 @@ export class IngestManagerPlugin return {}; } - public async start(core: CoreStart): Promise { - let successPromise: ReturnType; + public async start(core: CoreStart): Promise { + let successPromise: ReturnType; return { isInitialized: () => { diff --git a/x-pack/plugins/fleet/server/collectors/config_collectors.ts b/x-pack/plugins/fleet/server/collectors/config_collectors.ts index c201d1d4dfa25..8fb4924a2ccf0 100644 --- a/x-pack/plugins/fleet/server/collectors/config_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/config_collectors.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerConfigType } from '..'; +import { FleetConfigType } from '..'; -export const getIsFleetEnabled = (config: IngestManagerConfigType) => { +export const getIsFleetEnabled = (config: FleetConfigType) => { return config.agents.enabled; }; diff --git a/x-pack/plugins/fleet/server/collectors/register.ts b/x-pack/plugins/fleet/server/collectors/register.ts index cb39e6a5be579..e7d95a7e83773 100644 --- a/x-pack/plugins/fleet/server/collectors/register.ts +++ b/x-pack/plugins/fleet/server/collectors/register.ts @@ -10,7 +10,7 @@ import { getIsFleetEnabled } from './config_collectors'; import { AgentUsage, getAgentUsage } from './agent_collectors'; import { getInternalSavedObjectsClient } from './helpers'; import { PackageUsage, getPackageUsage } from './package_collectors'; -import { IngestManagerConfigType } from '..'; +import { FleetConfigType } from '..'; interface Usage { fleet_enabled: boolean; @@ -20,7 +20,7 @@ interface Usage { export function registerIngestManagerUsageCollector( core: CoreSetup, - config: IngestManagerConfigType, + config: FleetConfigType, usageCollection: UsageCollectionSetup | undefined ): void { // usageCollection is an optional dependency, so make sure to return if it is not registered. diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 3d34e37592ddd..3d30acd3f8e01 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -5,7 +5,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; -import { IngestManagerPlugin } from './plugin'; +import { FleetPlugin } from './plugin'; import { AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS, AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL, @@ -14,12 +14,7 @@ import { export { default as apm } from 'elastic-apm-node'; export { AgentService, ESIndexPatternService, getRegistryUrl, PackageService } from './services'; -export { - IngestManagerSetupContract, - IngestManagerSetupDeps, - IngestManagerStartContract, - ExternalCallback, -} from './plugin'; +export { FleetSetupContract, FleetSetupDeps, FleetStartContract, ExternalCallback } from './plugin'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -65,10 +60,10 @@ export const config: PluginConfigDescriptor = { }), }; -export type IngestManagerConfigType = TypeOf; +export type FleetConfigType = TypeOf; export { PackagePolicyServiceInterface } from './services/package_policy'; export const plugin = (initializerContext: PluginInitializerContext) => { - return new IngestManagerPlugin(initializerContext); + return new FleetPlugin(initializerContext); }; diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index 18b58b5673651..c8aef287e4432 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -5,12 +5,12 @@ */ import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -import { IngestManagerAppContext } from './plugin'; +import { FleetAppContext } from './plugin'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { securityMock } from '../../security/server/mocks'; import { PackagePolicyServiceInterface } from './services/package_policy'; -export const createAppContextStartContractMock = (): IngestManagerAppContext => { +export const createAppContextStartContractMock = (): FleetAppContext => { return { encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), savedObjects: savedObjectsServiceMock.createStartContract(), diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index bf5b2aac50643..47692d478b760 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -51,7 +51,7 @@ import { registerSettingsRoutes, registerAppRoutes, } from './routes'; -import { EsAssetReference, IngestManagerConfigType, NewPackagePolicy } from '../common'; +import { EsAssetReference, FleetConfigType, NewPackagePolicy } from '../common'; import { appContextService, licenseService, @@ -72,7 +72,7 @@ import { agentCheckinState } from './services/agents/checkin/state'; import { registerIngestManagerUsageCollector } from './collectors/register'; import { getInstallation } from './services/epm/packages'; -export interface IngestManagerSetupDeps { +export interface FleetSetupDeps { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; features?: FeaturesPluginSetup; @@ -81,13 +81,13 @@ export interface IngestManagerSetupDeps { usageCollection?: UsageCollectionSetup; } -export type IngestManagerStartDeps = object; +export type FleetStartDeps = object; -export interface IngestManagerAppContext { +export interface FleetAppContext { encryptedSavedObjectsStart: EncryptedSavedObjectsPluginStart; encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginSetup; - config$?: Observable; + config$?: Observable; savedObjects: SavedObjectsServiceStart; isProductionMode: PluginInitializerContext['env']['mode']['prod']; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; @@ -97,7 +97,7 @@ export interface IngestManagerAppContext { httpSetup?: HttpServiceSetup; } -export type IngestManagerSetupContract = void; +export type FleetSetupContract = void; const allSavedObjectTypes = [ OUTPUT_SAVED_OBJECT_TYPE, @@ -110,7 +110,7 @@ const allSavedObjectTypes = [ ]; /** - * Callbacks supported by the Ingest plugin + * Callbacks supported by the Fleet plugin */ export type ExternalCallback = [ 'packagePolicyCreate', @@ -124,52 +124,46 @@ export type ExternalCallback = [ export type ExternalCallbacksStorage = Map>; /** - * Describes public IngestManager plugin contract returned at the `startup` stage. + * Describes public Fleet plugin contract returned at the `startup` stage. */ -export interface IngestManagerStartContract { +export interface FleetStartContract { esIndexPatternService: ESIndexPatternService; packageService: PackageService; agentService: AgentService; /** - * Services for Ingest's package policies + * Services for Fleet's package policies */ packagePolicyService: typeof packagePolicyService; /** - * Register callbacks for inclusion in ingest API processing + * Register callbacks for inclusion in fleet API processing * @param args */ registerExternalCallback: (...args: ExternalCallback) => void; } -export class IngestManagerPlugin - implements - Plugin< - IngestManagerSetupContract, - IngestManagerStartContract, - IngestManagerSetupDeps, - IngestManagerStartDeps - > { +export class FleetPlugin + implements Plugin { private licensing$!: Observable; - private config$: Observable; + private config$: Observable; private security: SecurityPluginSetup | undefined; private cloud: CloudSetup | undefined; private logger: Logger | undefined; - private isProductionMode: IngestManagerAppContext['isProductionMode']; - private kibanaVersion: IngestManagerAppContext['kibanaVersion']; - private kibanaBranch: IngestManagerAppContext['kibanaBranch']; + private isProductionMode: FleetAppContext['isProductionMode']; + private kibanaVersion: FleetAppContext['kibanaVersion']; + private kibanaBranch: FleetAppContext['kibanaBranch']; private httpSetup: HttpServiceSetup | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; constructor(private readonly initializerContext: PluginInitializerContext) { - this.config$ = this.initializerContext.config.create(); + this.config$ = this.initializerContext.config.create(); this.isProductionMode = this.initializerContext.env.mode.prod; this.kibanaVersion = this.initializerContext.env.packageInfo.version; this.kibanaBranch = this.initializerContext.env.packageInfo.branch; this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + public async setup(core: CoreSetup, deps: FleetSetupDeps) { this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; if (deps.security) { @@ -186,15 +180,15 @@ export class IngestManagerPlugin if (deps.features) { deps.features.registerKibanaFeature({ id: PLUGIN_ID, - name: 'Ingest Manager', + name: 'Fleet', category: DEFAULT_APP_CATEGORIES.management, app: [PLUGIN_ID, 'kibana'], - catalogue: ['ingestManager'], + catalogue: ['fleet'], privileges: { all: { api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`], app: [PLUGIN_ID, 'kibana'], - catalogue: ['ingestManager'], + catalogue: ['fleet'], savedObject: { all: allSavedObjectTypes, read: [], @@ -204,7 +198,7 @@ export class IngestManagerPlugin read: { api: [`${PLUGIN_ID}-read`], app: [PLUGIN_ID, 'kibana'], - catalogue: ['ingestManager'], // TODO: check if this is actually available to read user + catalogue: ['fleet'], // TODO: check if this is actually available to read user savedObject: { all: [], read: allSavedObjectTypes, @@ -264,7 +258,7 @@ export class IngestManagerPlugin plugins: { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; } - ): Promise { + ): Promise { await appContextService.start({ encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 2f97a6bcde42c..39b80c6d096de 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -55,7 +55,7 @@ import * as AgentService from '../../services/agents'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; import { appContextService } from '../../services'; import { postAgentUnenrollHandler, postBulkAgentsUnenrollHandler } from './unenroll_handler'; -import { IngestManagerConfigType } from '../..'; +import { FleetConfigType } from '../..'; import { postAgentUpgradeHandler, postBulkAgentsUpgradeHandler } from './upgrade_handler'; const ajv = new Ajv({ @@ -81,7 +81,7 @@ function makeValidator(jsonSchema: any) { }; } -export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => { +export const registerRoutes = (router: IRouter, config: FleetConfigType) => { // Get one router.get( { diff --git a/x-pack/plugins/fleet/server/routes/limited_concurrency.test.ts b/x-pack/plugins/fleet/server/routes/limited_concurrency.test.ts index cc358c32528c9..ff304d82cb50f 100644 --- a/x-pack/plugins/fleet/server/routes/limited_concurrency.test.ts +++ b/x-pack/plugins/fleet/server/routes/limited_concurrency.test.ts @@ -10,12 +10,12 @@ import { isLimitedRoute, registerLimitedConcurrencyRoutes, } from './limited_concurrency'; -import { IngestManagerConfigType } from '../index'; +import { FleetConfigType } from '../index'; describe('registerLimitedConcurrencyRoutes', () => { test(`doesn't call registerOnPreAuth if maxConcurrentConnections is 0`, async () => { const mockSetup = coreMock.createSetup(); - const mockConfig = { agents: { maxConcurrentConnections: 0 } } as IngestManagerConfigType; + const mockConfig = { agents: { maxConcurrentConnections: 0 } } as FleetConfigType; registerLimitedConcurrencyRoutes(mockSetup, mockConfig); expect(mockSetup.http.registerOnPreAuth).not.toHaveBeenCalled(); @@ -23,7 +23,7 @@ describe('registerLimitedConcurrencyRoutes', () => { test(`calls registerOnPreAuth once if maxConcurrentConnections is 1`, async () => { const mockSetup = coreMock.createSetup(); - const mockConfig = { agents: { maxConcurrentConnections: 1 } } as IngestManagerConfigType; + const mockConfig = { agents: { maxConcurrentConnections: 1 } } as FleetConfigType; registerLimitedConcurrencyRoutes(mockSetup, mockConfig); expect(mockSetup.http.registerOnPreAuth).toHaveBeenCalledTimes(1); @@ -31,7 +31,7 @@ describe('registerLimitedConcurrencyRoutes', () => { test(`calls registerOnPreAuth once if maxConcurrentConnections is 1000`, async () => { const mockSetup = coreMock.createSetup(); - const mockConfig = { agents: { maxConcurrentConnections: 1000 } } as IngestManagerConfigType; + const mockConfig = { agents: { maxConcurrentConnections: 1000 } } as FleetConfigType; registerLimitedConcurrencyRoutes(mockSetup, mockConfig); expect(mockSetup.http.registerOnPreAuth).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/fleet/server/routes/limited_concurrency.ts b/x-pack/plugins/fleet/server/routes/limited_concurrency.ts index 609428f5477f1..060d7d6b99050 100644 --- a/x-pack/plugins/fleet/server/routes/limited_concurrency.ts +++ b/x-pack/plugins/fleet/server/routes/limited_concurrency.ts @@ -11,7 +11,7 @@ import { OnPreAuthToolkit, } from 'kibana/server'; import { LIMITED_CONCURRENCY_ROUTE_TAG } from '../../common'; -import { IngestManagerConfigType } from '../index'; +import { FleetConfigType } from '../index'; export class MaxCounter { constructor(private readonly max: number = 1) {} @@ -74,7 +74,7 @@ export function createLimitedPreAuthHandler({ }; } -export function registerLimitedConcurrencyRoutes(core: CoreSetup, config: IngestManagerConfigType) { +export function registerLimitedConcurrencyRoutes(core: CoreSetup, config: FleetConfigType) { const max = config.agents.maxConcurrentConnections; if (!max) return; diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 56c2eab385291..4d6f375ddf160 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -9,7 +9,7 @@ import { httpServerMock } from 'src/core/server/mocks'; import { PostIngestSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; import { createAppContextStartContractMock } from '../../mocks'; -import { ingestManagerSetupHandler } from './handlers'; +import { FleetSetupHandler } from './handlers'; import { appContextService } from '../../services/app_context'; import { setupIngestManager } from '../../services/setup'; @@ -21,7 +21,7 @@ jest.mock('../../services/setup', () => { const mockSetupIngestManager = setupIngestManager as jest.MockedFunction; -describe('ingestManagerSetupHandler', () => { +describe('FleetSetupHandler', () => { let context: ReturnType; let response: ReturnType; let request: ReturnType; @@ -44,7 +44,7 @@ describe('ingestManagerSetupHandler', () => { it('POST /setup succeeds w/200 and body of resolved value', async () => { mockSetupIngestManager.mockImplementation(() => Promise.resolve({ isIntialized: true })); - await ingestManagerSetupHandler(context, request, response); + await FleetSetupHandler(context, request, response); const expectedBody: PostIngestSetupResponse = { isInitialized: true }; expect(response.customError).toHaveBeenCalledTimes(0); @@ -55,7 +55,7 @@ describe('ingestManagerSetupHandler', () => { mockSetupIngestManager.mockImplementation(() => Promise.reject(new Error('SO method mocked to throw')) ); - await ingestManagerSetupHandler(context, request, response); + await FleetSetupHandler(context, request, response); expect(response.customError).toHaveBeenCalledTimes(1); expect(response.customError).toHaveBeenCalledWith({ @@ -71,7 +71,7 @@ describe('ingestManagerSetupHandler', () => { Promise.reject(new RegistryError('Registry method mocked to throw')) ); - await ingestManagerSetupHandler(context, request, response); + await FleetSetupHandler(context, request, response); expect(response.customError).toHaveBeenCalledTimes(1); expect(response.customError).toHaveBeenCalledWith({ statusCode: 502, diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index 0bd7b4e875062..b2ad9591bc2ee 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -72,7 +72,7 @@ export const createFleetSetupHandler: RequestHandler< } }; -export const ingestManagerSetupHandler: RequestHandler = async (context, request, response) => { +export const FleetSetupHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; diff --git a/x-pack/plugins/fleet/server/routes/setup/index.ts b/x-pack/plugins/fleet/server/routes/setup/index.ts index 6672a7e8933a8..35715600d37df 100644 --- a/x-pack/plugins/fleet/server/routes/setup/index.ts +++ b/x-pack/plugins/fleet/server/routes/setup/index.ts @@ -6,15 +6,11 @@ import { IRouter } from 'src/core/server'; import { PLUGIN_ID, AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; -import { IngestManagerConfigType } from '../../../common'; -import { - getFleetStatusHandler, - createFleetSetupHandler, - ingestManagerSetupHandler, -} from './handlers'; +import { FleetConfigType } from '../../../common'; +import { getFleetStatusHandler, createFleetSetupHandler, FleetSetupHandler } from './handlers'; import { PostFleetSetupRequestSchema } from '../../types'; -export const registerIngestManagerSetupRoute = (router: IRouter) => { +export const registerFleetSetupRoute = (router: IRouter) => { router.post( { path: SETUP_API_ROUTE, @@ -23,7 +19,7 @@ export const registerIngestManagerSetupRoute = (router: IRouter) => { // and will see `Unable to initialize Ingest Manager` in the UI options: { tags: [`access:${PLUGIN_ID}-read`] }, }, - ingestManagerSetupHandler + FleetSetupHandler ); }; @@ -49,9 +45,9 @@ export const registerGetFleetStatusRoute = (router: IRouter) => { ); }; -export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => { +export const registerRoutes = (router: IRouter, config: FleetConfigType) => { // Ingest manager setup - registerIngestManagerSetupRoute(router); + registerFleetSetupRoute(router); if (!config.agents.enabled) { return; diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 7f82670a4d02c..5c4e33d50b480 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -12,26 +12,26 @@ import { } from '../../../encrypted_saved_objects/server'; import packageJSON from '../../../../../package.json'; import { SecurityPluginSetup } from '../../../security/server'; -import { IngestManagerConfigType } from '../../common'; -import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin'; +import { FleetConfigType } from '../../common'; +import { ExternalCallback, ExternalCallbacksStorage, FleetAppContext } from '../plugin'; import { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; private security: SecurityPluginSetup | undefined; - private config$?: Observable; - private configSubject$?: BehaviorSubject; + private config$?: Observable; + private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; - private isProductionMode: IngestManagerAppContext['isProductionMode'] = false; - private kibanaVersion: IngestManagerAppContext['kibanaVersion'] = packageJSON.version; - private kibanaBranch: IngestManagerAppContext['kibanaBranch'] = packageJSON.branch; + private isProductionMode: FleetAppContext['isProductionMode'] = false; + private kibanaVersion: FleetAppContext['kibanaVersion'] = packageJSON.version; + private kibanaBranch: FleetAppContext['kibanaBranch'] = packageJSON.branch; private cloud?: CloudSetup; private logger: Logger | undefined; private httpSetup?: HttpServiceSetup; private externalCallbacks: ExternalCallbacksStorage = new Map(); - public async start(appContext: IngestManagerAppContext) { + public async start(appContext: FleetAppContext) { this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); this.encryptedSavedObjectsSetup = appContext.encryptedSavedObjectsSetup; this.security = appContext.security; diff --git a/x-pack/plugins/fleet/server/services/config.ts b/x-pack/plugins/fleet/server/services/config.ts index 23cd38cc123ce..f1f5611a20a0f 100644 --- a/x-pack/plugins/fleet/server/services/config.ts +++ b/x-pack/plugins/fleet/server/services/config.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { Observable, Subscription } from 'rxjs'; -import { IngestManagerConfigType } from '../'; +import { FleetConfigType } from '../'; /** * Kibana config observable service, *NOT* agent policy */ class ConfigService { - private observable: Observable | null = null; + private observable: Observable | null = null; private subscription: Subscription | null = null; - private config: IngestManagerConfigType | null = null; + private config: FleetConfigType | null = null; - private updateInformation(config: IngestManagerConfigType) { + private updateInformation(config: FleetConfigType) { this.config = config; } - public start(config$: Observable) { + public start(config$: Observable) { this.observable = config$; this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); } diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index a76d5dc99cbaf..8ce307c103f4c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -67,9 +67,9 @@ describe('Data Streams tab', () => { expect(exists('templateList')).toBe(true); }); - test('when Ingest Manager is enabled, links to Ingest Manager', async () => { + test('when Fleet is enabled, links to Fleet', async () => { testBed = await setup({ - plugins: { ingestManager: { hi: 'ok' } }, + plugins: { fleet: { hi: 'ok' } }, }); await act(async () => { @@ -80,7 +80,7 @@ describe('Data Streams tab', () => { component.update(); // Assert against the text because the href won't be available, due to dependency upon our core mock. - expect(findEmptyPromptIndexTemplateLink().text()).toBe('Ingest Manager'); + expect(findEmptyPromptIndexTemplateLink().text()).toBe('Fleet'); }); }); diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index 4e4ad9b8e1d31..5dcff0ba942e1 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -3,18 +3,8 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "home", - "licensing", - "management", - "features", - "share" - ], - "optionalPlugins": [ - "security", - "usageCollection", - "ingestManager" - ], + "requiredPlugins": ["home", "licensing", "management", "features", "share"], + "optionalPlugins": ["security", "usageCollection", "fleet"], "configPath": ["xpack", "index_management"], "requiredBundles": [ "kibanaReact", diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 5094aa2763a01..c9337767365fa 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -10,7 +10,7 @@ import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { CoreSetup, CoreStart } from '../../../../../src/core/public'; -import { IngestManagerSetup } from '../../../fleet/public'; +import { FleetSetup } from '../../../fleet/public'; import { IndexMgmtMetricsType } from '../types'; import { UiMetricService, NotificationService, HttpService } from './services'; import { ExtensionsService } from '../services'; @@ -25,7 +25,7 @@ export interface AppDependencies { }; plugins: { usageCollection: UsageCollectionSetup; - ingestManager?: IngestManagerSetup; + fleet?: FleetSetup; }; services: { uiMetricService: UiMetricService; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index c15af4f19827b..13e25f6d29a14 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -9,7 +9,7 @@ import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from 'src/plugins/management/public/'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import { IngestManagerSetup } from '../../../fleet/public'; +import { FleetSetup } from '../../../fleet/public'; import { PLUGIN } from '../../common/constants'; import { ExtensionsService } from '../services'; import { IndexMgmtMetricsType, StartDependencies } from '../types'; @@ -32,7 +32,7 @@ export async function mountManagementSection( usageCollection: UsageCollectionSetup, services: InternalServices, params: ManagementAppMountParams, - ingestManager?: IngestManagerSetup + fleet?: FleetSetup ) { const { element, setBreadcrumbs, history } = params; const [core, startDependencies] = await coreSetup.getStartServices(); @@ -57,7 +57,7 @@ export async function mountManagementSection( }, plugins: { usageCollection, - ingestManager, + fleet, }, services, history, diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index 0df5697a4281a..bc7df7a70196e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -49,7 +49,7 @@ export const DataStreamList: React.FunctionComponent {' ' /* We need this space to separate these two sentences. */} - {ingestManager ? ( + {fleet ? ( {i18n.translate( 'xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIngestManagerLink', { - defaultMessage: 'Ingest Manager', + defaultMessage: 'Fleet', } )} diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 855486528b797..58103688e6103 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -40,7 +40,7 @@ export class IndexMgmtUIPlugin { plugins: SetupDependencies ): IndexManagementPluginSetup { const { http, notifications } = coreSetup; - const { ingestManager, usageCollection, management } = plugins; + const { fleet, usageCollection, management } = plugins; httpService.setup(http); notificationService.setup(notifications); @@ -58,7 +58,7 @@ export class IndexMgmtUIPlugin { uiMetricService: this.uiMetricService, extensionsService: this.extensionsService, }; - return mountManagementSection(coreSetup, usageCollection, services, params, ingestManager); + return mountManagementSection(coreSetup, usageCollection, services, params, fleet); }, }); diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts index 34d060d935415..ee763ac83697c 100644 --- a/x-pack/plugins/index_management/public/types.ts +++ b/x-pack/plugins/index_management/public/types.ts @@ -5,7 +5,7 @@ */ import { ExtensionsSetup } from './services'; -import { IngestManagerSetup } from '../../fleet/public'; +import { FleetSetup } from '../../fleet/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { SharePluginStart } from '../../../../src/plugins/share/public'; @@ -17,7 +17,7 @@ export interface IndexManagementPluginSetup { } export interface SetupDependencies { - ingestManager?: IngestManagerSetup; + fleet?: FleetSetup; usageCollection: UsageCollectionSetup; management: ManagementSetup; } diff --git a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx similarity index 74% rename from x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx rename to x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx index 5d0c8a40ed3de..dfe683cf82c86 100644 --- a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/fleet_panel/index.tsx @@ -13,14 +13,14 @@ import { EuiText } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; import { usePluginContext } from '../../../hooks/use_plugin_context'; -export function IngestManagerPanel() { +export function FleetPanel() { const { core } = usePluginContext(); return ( @@ -28,24 +28,24 @@ export function IngestManagerPanel() {

- {i18n.translate('xpack.observability.ingestManager.title', { - defaultMessage: 'Have you seen our new Ingest Manager?', + {i18n.translate('xpack.observability.fleet.title', { + defaultMessage: 'Have you seen our new Fleet?', })}

- {i18n.translate('xpack.observability.ingestManager.text', { + {i18n.translate('xpack.observability.fleet.text', { defaultMessage: 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', })} - - {i18n.translate('xpack.observability.ingestManager.button', { - defaultMessage: 'Try Ingest Manager Beta', + + {i18n.translate('xpack.observability.fleet.button', { + defaultMessage: 'Try Fleet Beta', })} diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 24620f641c204..7377a1ca0ea52 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import styled, { ThemeContext } from 'styled-components'; -import { IngestManagerPanel } from '../../components/app/ingest_manager_panel'; +import { FleetPanel } from '../../components/app/fleet_panel'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTrackPageview } from '../../hooks/use_track_metric'; @@ -122,7 +122,7 @@ export function LandingPage() { - + diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 145e34c4fc99c..e7dbe6e46686e 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -20,7 +20,7 @@ ], "optionalPlugins": [ "encryptedSavedObjects", - "ingestManager", + "fleet", "ml", "newsfeed", "security", @@ -33,5 +33,5 @@ ], "server": true, "ui": true, - "requiredBundles": ["esUiShared", "ingestManager", "kibanaUtils", "kibanaReact", "lists", "ml"] + "requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact", "lists", "ml"] } diff --git a/x-pack/plugins/security_solution/public/app/home/setup.tsx b/x-pack/plugins/security_solution/public/app/home/setup.tsx index c3567e34a0411..1ec62d63bd7f3 100644 --- a/x-pack/plugins/security_solution/public/app/home/setup.tsx +++ b/x-pack/plugins/security_solution/public/app/home/setup.tsx @@ -6,12 +6,12 @@ import * as React from 'react'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; -import { IngestManagerStart } from '../../../../fleet/public'; +import { FleetStart } from '../../../../fleet/public'; export const Setup: React.FunctionComponent<{ - ingestManager: IngestManagerStart; + fleet: FleetStart; notifications: NotificationsStart; -}> = ({ ingestManager, notifications }) => { +}> = ({ fleet, notifications }) => { React.useEffect(() => { const defaultText = i18n.translate('xpack.securitySolution.endpoint.ingestToastMessage', { defaultMessage: 'Ingest Manager failed during its setup.', @@ -32,8 +32,8 @@ export const Setup: React.FunctionComponent<{ }); }; - ingestManager.isInitialized().catch((error: Error) => displayToastWithModal(error.message)); - }, [ingestManager, notifications.toasts]); + fleet.isInitialized().catch((error: Error) => displayToastWithModal(error.message)); + }, [fleet, notifications.toasts]); return null; }; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap index 6838b673b90d8..da8f0d8dcb6b7 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/link_to_app.test.tsx.snap @@ -2,16 +2,16 @@ exports[`LinkToApp component should render with href 1`] = ` @@ -23,7 +23,7 @@ exports[`LinkToApp component should render with href 1`] = ` exports[`LinkToApp component should render with minimum input 1`] = ` { }); it('should render with minimum input', () => { - expect(render({'link'})).toMatchSnapshot(); + expect(render({'link'})).toMatchSnapshot(); }); it('should render with href', () => { expect( render( - + {'link'} ) @@ -46,7 +46,7 @@ describe('LinkToApp component', () => { // Take `_event` (even though it is not used) so that `jest.fn` will have a type that expects to be called with an event const spyOnClickHandler: LinkToAppOnClickMock = jest.fn().mockImplementation((_event) => {}); const renderResult = render( - + {'link'} ); @@ -57,19 +57,19 @@ describe('LinkToApp component', () => { expect(spyOnClickHandler).toHaveBeenCalled(); expect(clickEventArg.preventDefault).toBeInstanceOf(Function); expect(clickEventArg.isDefaultPrevented()).toBe(true); - expect(fakeCoreStart.application.navigateToApp).toHaveBeenCalledWith('ingestManager', { + expect(fakeCoreStart.application.navigateToApp).toHaveBeenCalledWith('fleet', { path: undefined, state: undefined, }); }); it('should navigate to App with specific path', () => { const renderResult = render( - + {'link'} ); renderResult.find('EuiLink').simulate('click', { button: 0 }); - expect(fakeCoreStart.application.navigateToApp).toHaveBeenCalledWith('ingestManager', { + expect(fakeCoreStart.application.navigateToApp).toHaveBeenCalledWith('fleet', { path: '/some/path', state: undefined, }); @@ -77,9 +77,9 @@ describe('LinkToApp component', () => { it('should passes through EuiLinkProps', () => { const renderResult = render( { className: 'my-class', color: 'primary', 'data-test-subj': 'my-test-subject', - href: '/app/ingest', + href: '/app/fleet', onClick: expect.any(Function), }); }); @@ -105,7 +105,7 @@ describe('LinkToApp component', () => { try { } catch (e) { const renderResult = render( - + {'link'} ); @@ -119,7 +119,7 @@ describe('LinkToApp component', () => { ev.preventDefault(); }); const renderResult = render( - + {'link'} ); @@ -127,13 +127,13 @@ describe('LinkToApp component', () => { expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled(); }); it('should not to navigate if it was not left click', () => { - const renderResult = render({'link'}); + const renderResult = render({'link'}); renderResult.find('EuiLink').simulate('click', { button: 1 }); expect(fakeCoreStart.application.navigateToApp).not.toHaveBeenCalled(); }); it('should not to navigate if it includes an anchor target', () => { const renderResult = render( - + {'link'} ); @@ -142,7 +142,7 @@ describe('LinkToApp component', () => { }); it('should not to navigate if if meta|alt|ctrl|shift keys are pressed', () => { const renderResult = render( - + {'link'} ); diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts index e48f48e501903..97e73380d9e2e 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts @@ -7,7 +7,7 @@ import { ApplicationStart } from 'src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; /** - * Returns an object which ingest permissions are allowed + * Returns an object which fleet permissions are allowed */ export const useIngestEnabledCheck = (): { allEnabled: boolean; @@ -17,12 +17,12 @@ export const useIngestEnabledCheck = (): { } => { const { services } = useKibana<{ application: ApplicationStart }>(); - // Check if Ingest Manager is present in the configuration - const show = Boolean(services.application.capabilities.ingestManager?.show); - const write = Boolean(services.application.capabilities.ingestManager?.write); - const read = Boolean(services.application.capabilities.ingestManager?.read); + // Check if Fleet is present in the configuration + const show = Boolean(services.application.capabilities.fleet?.show); + const write = Boolean(services.application.capabilities.fleet?.write); + const read = Boolean(services.application.capabilities.fleet?.read); - // Check if all Ingest Manager permissions are enabled + // Check if all Fleet permissions are enabled const allEnabled = show && read && write ? true : false; return { diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts index 943b30925a54c..30371f76f8eea 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts @@ -25,7 +25,7 @@ type EventHandlerCallback = MouseEventHandlerSee policies */ export const useNavigateToAppEventHandler = ( diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 1b9e95f7d0737..e55210e1dc09a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -29,7 +29,7 @@ export interface AppContextTestRender { store: Store; history: ReturnType; coreStart: ReturnType; - depsStart: Pick; + depsStart: Pick; middlewareSpy: MiddlewareActionSpyHelper; /** * A wrapper around `AppRootContext` component. Uses the mocked modules as input to the diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx index fd6a483e538b8..149d948a53fc4 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx @@ -24,7 +24,7 @@ export const AppRootProvider = memo<{ store: Store; history: History; coreStart: CoreStart; - depsStart: Pick; + depsStart: Pick; children: ReactNode | ReactNode[]; }>( ({ diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts index 3388fb5355845..864b5e9df8043 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerStart } from '../../../../../fleet/public'; +import { FleetStart } from '../../../../../fleet/public'; import { dataPluginMock, Start as DataPublicStartMock, @@ -33,7 +33,7 @@ type DataMock = Omit & { */ export interface DepsStartMock { data: DataMock; - ingestManager: IngestManagerStart; + fleet: FleetStart; } /** @@ -56,7 +56,7 @@ export const depsStartMock: () => DepsStartMock = () => { return { data: dataMock, - ingestManager: { + fleet: { isInitialized: () => Promise.resolve(true), registerExtension: jest.fn(), }, diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index 189aa05b91f4b..97cf14751cb26 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -76,7 +76,7 @@ export type ImmutableMiddleware = ( */ export type ImmutableMiddlewareFactory = ( coreStart: CoreStart, - depsStart: Pick + depsStart: Pick ) => ImmutableMiddleware; /** @@ -87,7 +87,7 @@ export type ImmutableMiddlewareFactory = ( */ export type SecuritySubPluginMiddlewareFactory = ( coreStart: CoreStart, - depsStart: Pick + depsStart: Pick ) => Array>>>; /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index a9c84678c88a9..012bbed25d747 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -24,22 +24,22 @@ export function useEndpointSelector(selector: (state: EndpointState) } /** - * Returns an object that contains Ingest app and URL information + * Returns an object that contains Fleet app and URL information */ export const useIngestUrl = (subpath: string): { url: string; appId: string; appPath: string } => { const { services } = useKibana(); return useMemo(() => { const appPath = `#/${subpath}`; return { - url: `${services.application.getUrlForApp('ingestManager')}${appPath}`, - appId: 'ingestManager', + url: `${services.application.getUrlForApp('fleet')}${appPath}`, + appId: 'fleet', appPath, }; }, [services.application, subpath]); }; /** - * Returns an object that contains Ingest app and URL information + * Returns an object that contains Fleet app and URL information */ export const useAgentDetailsIngestUrl = ( agentId: string @@ -48,8 +48,8 @@ export const useAgentDetailsIngestUrl = ( return useMemo(() => { const appPath = `#/fleet/agents/${agentId}/activity`; return { - url: `${services.application.getUrlForApp('ingestManager')}${appPath}`, - appId: 'ingestManager', + url: `${services.application.getUrlForApp('fleet')}${appPath}`, + appId: 'fleet', appPath, }; }, [services.application, agentId]); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index d785e3b3a131a..4b955f2fe2959 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -612,19 +612,19 @@ describe('when on the list page', () => { }); it('should include the link to reassignment in Ingest', async () => { - coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); + coreStart.application.getUrlForApp.mockReturnValue('/app/fleet'); const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest'); expect(linkToReassign).not.toBeNull(); expect(linkToReassign.textContent).toEqual('Reassign Policy'); expect(linkToReassign.getAttribute('href')).toEqual( - `/app/ingestManager#/fleet/agents/${elasticAgentId}/activity?openReassignFlyout=true` + `/app/fleet#/fleet/agents/${elasticAgentId}/activity?openReassignFlyout=true` ); }); describe('when link to reassignment in Ingest is clicked', () => { beforeEach(async () => { - coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); + coreStart.application.getUrlForApp.mockReturnValue('/app/fleet'); const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest'); reactTestingLibrary.act(() => { @@ -820,8 +820,8 @@ describe('when on the list page', () => { switch (appName) { case 'securitySolution': return '/app/security'; - case 'ingestManager': - return '/app/ingestManager'; + case 'fleet': + return '/app/fleet'; } return appName; }); @@ -852,9 +852,7 @@ describe('when on the list page', () => { }); const agentPolicyLink = await renderResult.findByTestId('agentPolicyLink'); - expect(agentPolicyLink.getAttribute('href')).toEqual( - `/app/ingestManager#/policies/${agentPolicyId}` - ); + expect(agentPolicyLink.getAttribute('href')).toEqual(`/app/fleet#/policies/${agentPolicyId}`); }); it('navigates to the Ingest Agent Details page', async () => { const renderResult = await renderAndWaitForData(); @@ -864,9 +862,7 @@ describe('when on the list page', () => { }); const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink'); - expect(agentDetailsLink.getAttribute('href')).toEqual( - `/app/ingestManager#/fleet/agents/${agentId}` - ); + expect(agentDetailsLink.getAttribute('href')).toEqual(`/app/fleet#/fleet/agents/${agentId}`); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index a37f256e359b9..2b40a7507da88 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -177,7 +177,7 @@ export const EndpointList = () => { ); const handleCreatePolicyClick = useNavigateToAppEventHandler( - 'ingestManager', + 'fleet', { path: `#/integrations${ endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-integration` : '' @@ -219,7 +219,7 @@ export const EndpointList = () => { const handleDeployEndpointsClick = useNavigateToAppEventHandler< AgentPolicyDetailsDeployAgentAction - >('ingestManager', { + >('fleet', { path: `#/policies/${selectedPolicyId}?openEnrollmentFlyout=true`, state: { onDoneNavigateTo: [ @@ -443,14 +443,14 @@ export const EndpointList = () => { icon="logoObservability" key="agentConfigLink" data-test-subj="agentPolicyLink" - navigateAppId="ingestManager" + navigateAppId="fleet" navigateOptions={{ path: `#${pagePathGetters.policy_details({ policyId: agentPolicies[item.metadata.Endpoint.policy.applied.id], })}`, }} href={`${services?.application?.getUrlForApp( - 'ingestManager' + 'fleet' )}#${pagePathGetters.policy_details({ policyId: agentPolicies[item.metadata.Endpoint.policy.applied.id], })}`} @@ -467,14 +467,14 @@ export const EndpointList = () => { icon="logoObservability" key="agentDetailsLink" data-test-subj="agentDetailsLink" - navigateAppId="ingestManager" + navigateAppId="fleet" navigateOptions={{ path: `#${pagePathGetters.fleet_agent_details({ agentId: item.metadata.elastic.agent.id, })}`, }} href={`${services?.application?.getUrlForApp( - 'ingestManager' + 'fleet' )}#${pagePathGetters.fleet_agent_details({ agentId: item.metadata.elastic.agent.id, })}`} @@ -591,12 +591,12 @@ export const EndpointList = () => { values={{ agentsLink: ( (() => { return [ - 'ingestManager', + 'fleet', { path: `#${pagePathGetters.edit_integration({ policyId: agentPolicyId, @@ -99,11 +99,11 @@ const EditFlowMessage = memo<{ path: getTrustedAppsListPath(), state: { backButtonUrl: navigateBackToIngest[1]?.path - ? `${getUrlForApp('ingestManager')}${navigateBackToIngest[1].path}` + ? `${getUrlForApp('fleet')}${navigateBackToIngest[1].path}` : undefined, onBackButtonNavigateTo: navigateBackToIngest, backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.trustedAppsMessageReturnBackLabel', + 'xpack.securitySolution.endpoint.fleet.editPackagePolicy.trustedAppsMessageReturnBackLabel', { defaultMessage: 'Back to Edit Integration' } ), }, @@ -120,7 +120,7 @@ const EditFlowMessage = memo<{ data-test-subj="endpointActions" > @@ -135,7 +135,7 @@ const EditFlowMessage = memo<{ data-test-subj="securityPolicy" > , @@ -145,7 +145,7 @@ const EditFlowMessage = memo<{ data-test-subj="trustedAppsAction" > , @@ -156,7 +156,7 @@ const EditFlowMessage = memo<{ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 274032eea0c5d..a3d6cbea3ddc7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -147,7 +147,7 @@ export const PolicyList = React.memo(() => { } = usePolicyListSelector(selector); const handleCreatePolicyClick = useNavigateToAppEventHandler( - 'ingestManager', + 'fleet', { // We redirect to Ingest's Integaration page if we can't get the package version, and // to the Integration Endpoint Package Add Integration if we have package information. @@ -339,9 +339,9 @@ export const PolicyList = React.memo(() => { diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 5cc0d79a3f9a3..f97bec65d269a 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -331,8 +331,8 @@ export class Plugin implements IPlugin + Pick > & { logger: Logger; manifestManager?: ManifestManager; @@ -74,7 +74,7 @@ export type EndpointAppContextServiceStartContract = Partial< security: SecurityPluginSetup; alerts: AlertsPluginStartContract; config: ConfigType; - registerIngestCallback?: IngestManagerStartContract['registerExternalCallback']; + registerIngestCallback?: FleetStartContract['registerExternalCallback']; savedObjectsStart: SavedObjectsServiceStart; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 588404fd516d0..7a1a0f06a2267 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -11,7 +11,7 @@ import { alertsMock } from '../../../alerts/server/mocks'; import { xpackMocks } from '../../../../mocks'; import { AgentService, - IngestManagerStartContract, + FleetStartContract, ExternalCallback, PackageService, } from '../../../fleet/server'; @@ -74,8 +74,8 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< alerts: alertsMock.createStart(), config, registerIngestCallback: jest.fn< - ReturnType, - Parameters + ReturnType, + Parameters >(), }; }; @@ -109,9 +109,7 @@ export const createMockAgentService = (): jest.Mocked => { * @param indexPattern a string index pattern to return when called by a test * @returns the same value as `indexPattern` parameter */ -export const createMockIngestManagerStartContract = ( - indexPattern: string -): IngestManagerStartContract => { +export const createMockFleetStartContract = (indexPattern: string): FleetStartContract => { return { esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 46a4363936b3d..1f90c689a688f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -55,7 +55,7 @@ describe('test endpoint route', () => { let routeHandler: RequestHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any let routeConfig: RouteConfig; - // tests assume that ingestManager is enabled, and thus agentService is available + // tests assume that fleet is enabled, and thus agentService is available let mockAgentService: Required< ReturnType >['agentService']; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts index 26f216f0474c2..2c7d1e9e48404 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts @@ -50,7 +50,7 @@ describe('test endpoint route v1', () => { let routeHandler: RequestHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any let routeConfig: RouteConfig; - // tests assume that ingestManager is enabled, and thus agentService is available + // tests assume that fleet is enabled, and thus agentService is available let mockAgentService: Required< ReturnType >['agentService']; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 036c94cf50050..8a33b1df4caa8 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -34,7 +34,7 @@ import { ListPluginSetup } from '../../lists/server'; import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; import { ILicense, LicensingPluginStart } from '../../licensing/server'; -import { IngestManagerStartContract, ExternalCallback } from '../../fleet/server'; +import { FleetStartContract, ExternalCallback } from '../../fleet/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -93,7 +93,7 @@ export interface SetupPlugins { export interface StartPlugins { alerts: AlertPluginStartContract; data: DataPluginStart; - ingestManager?: IngestManagerStartContract; + fleet?: FleetStartContract; licensing: LicensingPluginStart; taskManager?: TaskManagerStartContract; telemetry?: TelemetryPluginStart; @@ -326,27 +326,27 @@ export class Plugin implements IPlugin void) | undefined; const exceptionListsStartEnabled = () => { - return this.lists && plugins.taskManager && plugins.ingestManager; + return this.lists && plugins.taskManager && plugins.fleet; }; if (exceptionListsStartEnabled()) { const exceptionListClient = this.lists!.getExceptionListClient(savedObjectsClient, 'kibana'); const artifactClient = new ArtifactClient(savedObjectsClient); - registerIngestCallback = plugins.ingestManager!.registerExternalCallback; + registerIngestCallback = plugins.fleet!.registerExternalCallback; manifestManager = new ManifestManager({ savedObjectsClient, artifactClient, exceptionListClient, - packagePolicyService: plugins.ingestManager!.packagePolicyService, + packagePolicyService: plugins.fleet!.packagePolicyService, logger: this.logger, cache: this.exceptionsCache, }); } this.endpointAppContextService.start({ - agentService: plugins.ingestManager?.agentService, - packageService: plugins.ingestManager?.packageService, + agentService: plugins.fleet?.agentService, + packageService: plugins.fleet?.packageService, appClientFactory: this.appClientFactory, security: this.setupPlugins!.security!, alerts: plugins.alerts, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 287bb33fbb11a..5277944afa907 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15140,10 +15140,6 @@ "xpack.observability.home.sectionsubtitle": "ログ、メトリック、トレースを大規模に、1つのスタックにまとめて、環境内のあらゆる場所で生じるイベントの監視、分析、対応を行います。", "xpack.observability.home.sectionTitle": "エコシステム全体の一元的な可視性", "xpack.observability.home.title": "オブザーバビリティ", - "xpack.observability.ingestManager.beta": "ベータ", - "xpack.observability.ingestManager.button": "Ingest Managerベータを試す", - "xpack.observability.ingestManager.text": "Elasticエージェントでは、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsと他のエージェントをインストールする必要はありません。このため、インフラストラクチャ全体での構成のデプロイが簡単で高速になりました。", - "xpack.observability.ingestManager.title": "新しいIngest Managerをご覧になりましたか?", "xpack.observability.landing.breadcrumb": "はじめて使う", "xpack.observability.news.readFullStory": "詳細なストーリーを読む", "xpack.observability.news.title": "新機能", @@ -17445,12 +17441,6 @@ "xpack.securitySolution.endpoint.details.policyResponse.workflow": "ワークフロー", "xpack.securitySolution.endpoint.details.policyStatus": "ポリシー応答", "xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失敗} other {不明}}", - "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "推奨のデフォルト値で統合が保存されます。後からこれを変更するには、エージェントポリシー内でEndpoint Security統合を編集します。", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionSecurityPolicy": "セキュリティポリシーを編集", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionTrustedApps": "信頼できるアプリケーションを表示", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.menuButton": "アクション", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.message": "詳細構成オプションを表示するには、メニューからアクションを選択します。", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.trustedAppsMessageReturnBackLabel": "統合の編集に戻る", "xpack.securitySolution.endpoint.ingestToastMessage": "Ingest Managerが設定中に失敗しました。", "xpack.securitySolution.endpoint.ingestToastTitle": "アプリを初期化できませんでした", "xpack.securitySolution.endpoint.list.actionmenu": "開く", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 58448786c1143..0ac240ddfa13f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15158,10 +15158,6 @@ "xpack.observability.home.sectionsubtitle": "通过根据需要将日志、指标和跟踪都置于单个堆栈上,来监测、分析和响应环境中任何位置发生的事件。", "xpack.observability.home.sectionTitle": "整个生态系统的统一可见性", "xpack.observability.home.title": "可观测性", - "xpack.observability.ingestManager.beta": "公测版", - "xpack.observability.ingestManager.button": "试用采集管理器公测版", - "xpack.observability.ingestManager.text": "通过 Elastic 代理,可以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats 和其他代理,这简化和加快了将配置部署到整个基础设施的过程。", - "xpack.observability.ingestManager.title": "是否见过我们的新型采集管理器?", "xpack.observability.landing.breadcrumb": "入门", "xpack.observability.news.readFullStory": "详细了解", "xpack.observability.news.title": "最近的新闻", @@ -17463,12 +17459,6 @@ "xpack.securitySolution.endpoint.details.policyResponse.workflow": "工作流", "xpack.securitySolution.endpoint.details.policyStatus": "策略响应", "xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失败} other {未知}}", - "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "我们将使用建议的默认值保存您的集成。稍后,您可以通过在代理策略中编辑 Endpoint Security 集成对其进行更改。", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionSecurityPolicy": "编辑安全策略", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.actionTrustedApps": "查看受信任的应用程序", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.menuButton": "操作", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.message": "通过从菜单中选择操作可找到更多高级配置选项", - "xpack.securitySolution.endpoint.ingestManager.editPackagePolicy.trustedAppsMessageReturnBackLabel": "返回以编辑集成", "xpack.securitySolution.endpoint.ingestToastMessage": "采集管理器在其设置期间失败。", "xpack.securitySolution.endpoint.ingestToastTitle": "应用无法初始化", "xpack.securitySolution.endpoint.list.actionmenu": "打开", diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index bc1df21773a71..4b1c8c073b5ee 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -115,7 +115,7 @@ export default function ({ getService }: FtrProviderContext) { 'maps', 'uptime', 'siem', - 'ingestManager', + 'fleet', ].sort() ); }); diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index b6f77e9842296..843dd983adf85 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -38,7 +38,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], ml: ['all', 'read'], siem: ['all', 'read'], - ingestManager: ['all', 'read'], + fleet: ['all', 'read'], stackAlerts: ['all', 'read'], actions: ['all', 'read'], }, diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 679e96dd21514..5df4d597efaaa 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], ml: ['all', 'read'], siem: ['all', 'read'], - ingestManager: ['all', 'read'], + fleet: ['all', 'read'], stackAlerts: ['all', 'read'], actions: ['all', 'read'], }, diff --git a/x-pack/test/fleet_api_integration/apis/agents/delete.ts b/x-pack/test/fleet_api_integration/apis/agents/delete.ts index 39f518cb93696..b12a4513faef9 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/delete.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/delete.ts @@ -15,7 +15,7 @@ export default function ({ getService }: FtrProviderContext) { fleet_user: { permissions: { feature: { - ingestManager: ['read'], + fleet: ['read'], }, spaces: ['*'], }, @@ -25,7 +25,7 @@ export default function ({ getService }: FtrProviderContext) { fleet_admin: { permissions: { feature: { - ingestManager: ['all'], + fleet: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index cb7d97f49c9e1..e6a62274d34ab 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -26,7 +26,7 @@ export default function ({ getService }: FtrProviderContext) { fleet_user: { permissions: { feature: { - ingestManager: ['read'], + fleet: ['read'], }, spaces: ['*'], }, @@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) { fleet_admin: { permissions: { feature: { - ingestManager: ['all'], + fleet: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts index 38ba50b08d507..747b62a9550c6 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_policy_page.ts @@ -98,7 +98,7 @@ export function IngestManagerCreatePackagePolicy({ * Navigates to the Ingest Agent configuration Edit Package Policy page */ async navigateToAgentPolicyEditPackagePolicy(agentPolicyId: string, packagePolicyId: string) { - await pageObjects.common.navigateToApp('ingestManager', { + await pageObjects.common.navigateToApp('fleet', { hash: `/policies/${agentPolicyId}/edit-integration/${packagePolicyId}`, }); await this.ensureOnEditPageOrFail(); From cb7a59f9d77ab66fc8ed6f63c4b82b94a2c7556c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 19 Nov 2020 17:50:54 +0000 Subject: [PATCH 16/16] [ML] Space management UI (#83320) (#83810) * [ML] Space management UI * fixing types * small react refactor * adding repair toasts * text and style changes * handling spaces being disabled * correcting initalizing endpoint response * text updates * text updates * fixing spaces manager use when spaces is disabled * more text updates * switching to delete saved object first rather than overwrite * filtering non ml spaces * renaming file * fixing types * updating list style Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/ml/common/types/saved_objects.ts | 22 +- x-pack/plugins/ml/kibana.json | 3 +- .../components/job_spaces_list/index.ts | 2 +- .../job_spaces_list/job_spaces_list.tsx | 68 +++++- .../components/job_spaces_repair/index.ts | 7 + .../job_spaces_repair_flyout.tsx | 161 +++++++++++++ .../job_spaces_repair/repair_list.tsx | 182 ++++++++++++++ .../cannot_edit_callout.tsx | 29 +++ .../components/job_spaces_selector/index.ts | 7 + .../jobs_spaces_flyout.tsx | 131 +++++++++++ .../job_spaces_selector/spaces_selector.scss | 3 + .../job_spaces_selector/spaces_selectors.tsx | 222 ++++++++++++++++++ .../application/contexts/kibana/index.ts | 1 + .../contexts/kibana/use_ml_api_context.ts | 11 + .../application/contexts/spaces/index.ts | 12 + .../contexts/spaces/spaces_context.ts | 35 +++ .../analytics_list/analytics_list.tsx | 8 +- .../components/analytics_list/common.ts | 2 +- .../components/analytics_list/use_columns.tsx | 32 ++- .../analytics_service/get_analytics.ts | 2 +- .../components/jobs_list/jobs_list.js | 25 +- .../jobs_list_view/jobs_list_view.js | 12 +- .../jobs_list_page/jobs_list_page.tsx | 141 ++++++----- .../application/management/jobs_list/index.ts | 18 +- .../services/ml_api_service/saved_objects.ts | 21 +- x-pack/plugins/ml/public/plugin.ts | 2 + .../plugins/ml/server/saved_objects/repair.ts | 33 ++- .../ml/server/saved_objects/service.ts | 19 +- .../plugins/ml/server/saved_objects/util.ts | 4 + x-pack/plugins/spaces/public/index.ts | 2 + 30 files changed, 1096 insertions(+), 121 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx create mode 100644 x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts create mode 100644 x-pack/plugins/ml/public/application/contexts/spaces/index.ts create mode 100644 x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index dde235476f1f9..9f4d402ec1759 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -7,11 +7,23 @@ export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; -type Result = Record; +export interface SavedObjectResult { + [jobId: string]: { success: boolean; error?: any }; +} export interface RepairSavedObjectResponse { - savedObjectsCreated: Result; - savedObjectsDeleted: Result; - datafeedsAdded: Result; - datafeedsRemoved: Result; + savedObjectsCreated: SavedObjectResult; + savedObjectsDeleted: SavedObjectResult; + datafeedsAdded: SavedObjectResult; + datafeedsRemoved: SavedObjectResult; +} + +export type JobsSpacesResponse = { + [jobType in JobType]: { [jobId: string]: string[] }; +}; + +export interface InitializeSavedObjectResponse { + jobs: Array<{ id: string; type: string }>; + success: boolean; + error?: any; } diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 1cd52079b4e39..8ec9b8ee976d4 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -34,7 +34,8 @@ "kibanaReact", "dashboard", "savedObjects", - "home" + "home", + "spaces" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts b/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts index d154d82a8ee7f..f8b851e4fee35 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts +++ b/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { JobSpacesList } from './job_spaces_list'; +export { JobSpacesList, ALL_SPACES_ID } from './job_spaces_list'; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx index b362c87a12210..fa8d65d3e79fd 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx @@ -4,20 +4,64 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { JobSpacesFlyout } from '../job_spaces_selector'; +import { JobType } from '../../../../common/types/saved_objects'; +import { useSpacesContext } from '../../contexts/spaces'; +import { Space, SpaceAvatar } from '../../../../../spaces/public'; + +export const ALL_SPACES_ID = '*'; interface Props { - spaces: string[]; + spaceIds: string[]; + jobId: string; + jobType: JobType; + refresh(): void; +} + +function filterUnknownSpaces(ids: string[]) { + return ids.filter((id) => id !== '?'); } -export const JobSpacesList: FC = ({ spaces }) => ( - - {spaces.map((space) => ( - - {space} - - ))} - -); +export const JobSpacesList: FC = ({ spaceIds, jobId, jobType, refresh }) => { + const { allSpaces } = useSpacesContext(); + + const [showFlyout, setShowFlyout] = useState(false); + const [spaces, setSpaces] = useState([]); + + useEffect(() => { + const tempSpaces = spaceIds.includes(ALL_SPACES_ID) + ? [{ id: ALL_SPACES_ID, name: ALL_SPACES_ID, disabledFeatures: [], color: '#DDD' }] + : allSpaces.filter((s) => spaceIds.includes(s.id)); + setSpaces(tempSpaces); + }, [spaceIds, allSpaces]); + + function onClose() { + setShowFlyout(false); + refresh(); + } + + return ( + <> + setShowFlyout(true)} style={{ height: 'auto' }}> + + {spaces.map((space) => ( + + + + ))} + + + {showFlyout && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts b/x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts new file mode 100644 index 0000000000000..3a9c22c1f3688 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JobSpacesRepairFlyout } from './job_spaces_repair_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx new file mode 100644 index 0000000000000..47d3fe065dd66 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiText, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { ml } from '../../services/ml_api_service'; +import { + RepairSavedObjectResponse, + SavedObjectResult, +} from '../../../../common/types/saved_objects'; +import { RepairList } from './repair_list'; +import { useToastNotificationService } from '../../services/toast_notification_service'; + +interface Props { + onClose: () => void; +} +export const JobSpacesRepairFlyout: FC = ({ onClose }) => { + const { displayErrorToast, displaySuccessToast } = useToastNotificationService(); + const [loading, setLoading] = useState(false); + const [repairable, setRepairable] = useState(false); + const [repairResp, setRepairResp] = useState(null); + + async function loadRepairList(simulate: boolean = true) { + setLoading(true); + try { + const resp = await ml.savedObjects.repairSavedObjects(simulate); + setRepairResp(resp); + + const count = Object.values(resp).reduce((acc, cur) => acc + Object.keys(cur).length, 0); + setRepairable(count > 0); + setLoading(false); + return resp; + } catch (error) { + // this shouldn't be hit as errors are returned per-repair task + // as part of the response + displayErrorToast(error); + setLoading(false); + } + return null; + } + + useEffect(() => { + loadRepairList(); + }, []); + + async function repair() { + if (repairable) { + // perform the repair + const resp = await loadRepairList(false); + // check simulate the repair again to check that all + // items have been repaired. + await loadRepairList(true); + + if (resp === null) { + return; + } + const { successCount, errorCount } = getResponseCounts(resp); + if (errorCount > 0) { + const title = i18n.translate('xpack.ml.management.repairSavedObjectsFlyout.repair.error', { + defaultMessage: 'Some jobs cannot be repaired.', + }); + displayErrorToast(resp as any, title); + return; + } + + displaySuccessToast( + i18n.translate('xpack.ml.management.repairSavedObjectsFlyout.repair.success', { + defaultMessage: '{successCount} {successCount, plural, one {job} other {jobs}} repaired', + values: { successCount }, + }) + ); + } + } + + return ( + <> + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + ); +}; + +function getResponseCounts(resp: RepairSavedObjectResponse) { + let successCount = 0; + let errorCount = 0; + Object.values(resp).forEach((result: SavedObjectResult) => { + Object.values(result).forEach(({ success, error }) => { + if (success === true) { + successCount++; + } else if (error !== undefined) { + errorCount++; + } + }); + }); + return { successCount, errorCount }; +} diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx new file mode 100644 index 0000000000000..3eab255ba34e6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiText, EuiTitle, EuiAccordion, EuiTextColor, EuiHorizontalRule } from '@elastic/eui'; + +import { RepairSavedObjectResponse } from '../../../../common/types/saved_objects'; + +export const RepairList: FC<{ repairItems: RepairSavedObjectResponse | null }> = ({ + repairItems, +}) => { + if (repairItems === null) { + return null; + } + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; + +const SavedObjectsCreated: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.savedObjectsCreated); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const SavedObjectsDeleted: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.savedObjectsDeleted); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const DatafeedsAdded: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.datafeedsAdded); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const DatafeedsRemoved: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.datafeedsRemoved); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const RepairItem: FC<{ id: string; title: JSX.Element; items: string[] }> = ({ + id, + title, + items, +}) => ( + + + {items.length && ( +
    + {items.map((item) => ( +
  • {item}
  • + ))} +
+ )} +
+
+); diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx new file mode 100644 index 0000000000000..98473cf6a7f59 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +export const CannotEditCallout: FC<{ jobId: string }> = ({ jobId }) => ( + <> + + + + + +); diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts b/x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts new file mode 100644 index 0000000000000..fe1537f58531f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JobSpacesFlyout } from './jobs_spaces_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx new file mode 100644 index 0000000000000..9aa8942bce795 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { difference, xor } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, +} from '@elastic/eui'; + +import { JobType, SavedObjectResult } from '../../../../common/types/saved_objects'; +import { ml } from '../../services/ml_api_service'; +import { useToastNotificationService } from '../../services/toast_notification_service'; + +import { SpacesSelector } from './spaces_selectors'; + +interface Props { + jobId: string; + jobType: JobType; + spaceIds: string[]; + onClose: () => void; +} +export const JobSpacesFlyout: FC = ({ jobId, jobType, spaceIds, onClose }) => { + const { displayErrorToast } = useToastNotificationService(); + + const [selectedSpaceIds, setSelectedSpaceIds] = useState(spaceIds); + const [saving, setSaving] = useState(false); + const [savable, setSavable] = useState(false); + const [canEditSpaces, setCanEditSpaces] = useState(false); + + useEffect(() => { + const different = xor(selectedSpaceIds, spaceIds).length !== 0; + setSavable(different === true && selectedSpaceIds.length > 0); + }, [selectedSpaceIds.length]); + + async function applySpaces() { + if (savable) { + setSaving(true); + const addedSpaces = difference(selectedSpaceIds, spaceIds); + const removedSpaces = difference(spaceIds, selectedSpaceIds); + if (addedSpaces.length) { + const resp = await ml.savedObjects.assignJobToSpace(jobType, [jobId], addedSpaces); + handleApplySpaces(resp); + } + if (removedSpaces.length) { + const resp = await ml.savedObjects.removeJobFromSpace(jobType, [jobId], removedSpaces); + handleApplySpaces(resp); + } + onClose(); + } + } + + function handleApplySpaces(resp: SavedObjectResult) { + Object.entries(resp).forEach(([id, { success, error }]) => { + if (success === false) { + const title = i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.updateSpaces.error', + { + defaultMessage: 'Error updating {id}', + values: { id }, + } + ); + displayErrorToast(error, title); + } + }); + } + + return ( + <> + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + +
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss new file mode 100644 index 0000000000000..75cdbd972455b --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss @@ -0,0 +1,3 @@ +.mlCopyToSpace__spacesList { + margin-top: $euiSizeXS; +} diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx new file mode 100644 index 0000000000000..233b64dc1432e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './spaces_selector.scss'; +import React, { FC, useState, useEffect, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFormRow, + EuiSelectable, + EuiSelectableOption, + EuiIconTip, + EuiText, + EuiCheckableCard, + EuiFormFieldset, +} from '@elastic/eui'; + +import { SpaceAvatar } from '../../../../../spaces/public'; +import { useSpacesContext } from '../../contexts/spaces'; +import { ML_SAVED_OBJECT_TYPE } from '../../../../common/types/saved_objects'; +import { ALL_SPACES_ID } from '../job_spaces_list'; +import { CannotEditCallout } from './cannot_edit_callout'; + +type SpaceOption = EuiSelectableOption & { ['data-space-id']: string }; + +interface Props { + jobId: string; + spaceIds: string[]; + setSelectedSpaceIds: (ids: string[]) => void; + selectedSpaceIds: string[]; + canEditSpaces: boolean; + setCanEditSpaces: (canEditSpaces: boolean) => void; +} + +export const SpacesSelector: FC = ({ + jobId, + spaceIds, + setSelectedSpaceIds, + selectedSpaceIds, + canEditSpaces, + setCanEditSpaces, +}) => { + const { spacesManager, allSpaces } = useSpacesContext(); + + const [canShareToAllSpaces, setCanShareToAllSpaces] = useState(false); + + useEffect(() => { + if (spacesManager !== null) { + const getPermissions = spacesManager.getShareSavedObjectPermissions(ML_SAVED_OBJECT_TYPE); + Promise.all([getPermissions]).then(([{ shareToAllSpaces }]) => { + setCanShareToAllSpaces(shareToAllSpaces); + setCanEditSpaces(shareToAllSpaces || spaceIds.includes(ALL_SPACES_ID) === false); + }); + } + }, []); + + function toggleShareOption(isAllSpaces: boolean) { + const updatedSpaceIds = isAllSpaces + ? [ALL_SPACES_ID, ...selectedSpaceIds] + : selectedSpaceIds.filter((id) => id !== ALL_SPACES_ID); + setSelectedSpaceIds(updatedSpaceIds); + } + + function updateSelectedSpaces(selectedOptions: SpaceOption[]) { + const ids = selectedOptions.filter((opt) => opt.checked).map((opt) => opt['data-space-id']); + setSelectedSpaceIds(ids); + } + + const isGlobalControlChecked = useMemo(() => selectedSpaceIds.includes(ALL_SPACES_ID), [ + selectedSpaceIds, + ]); + + const options = useMemo( + () => + allSpaces.map((space) => { + return { + label: space.name, + prepend: , + checked: selectedSpaceIds.includes(space.id) ? 'on' : undefined, + disabled: canEditSpaces === false, + ['data-space-id']: space.id, + ['data-test-subj']: `mlSpaceSelectorRow_${space.id}`, + }; + }), + [allSpaces, selectedSpaceIds, canEditSpaces] + ); + + const shareToAllSpaces = useMemo( + () => ({ + id: 'shareToAllSpaces', + title: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.title', { + defaultMessage: 'All spaces', + }), + text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.text', { + defaultMessage: 'Make job available in all current and future spaces.', + }), + ...(!canShareToAllSpaces && { + tooltip: isGlobalControlChecked + ? i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotUncheckTooltip', + { defaultMessage: 'You need additional privileges to change this option.' } + ) + : i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotCheckTooltip', + { defaultMessage: 'You need additional privileges to use this option.' } + ), + }), + disabled: !canShareToAllSpaces, + }), + [isGlobalControlChecked, canShareToAllSpaces] + ); + + const shareToExplicitSpaces = useMemo( + () => ({ + id: 'shareToExplicitSpaces', + title: i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.title', + { + defaultMessage: 'Select spaces', + } + ), + text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.text', { + defaultMessage: 'Make job available in selected spaces only.', + }), + disabled: !canShareToAllSpaces && isGlobalControlChecked, + }), + [canShareToAllSpaces, isGlobalControlChecked] + ); + + return ( + <> + {canEditSpaces === false && } + + toggleShareOption(false)} + disabled={shareToExplicitSpaces.disabled} + > + + } + fullWidth + > + updateSelectedSpaces(newOptions as SpaceOption[])} + listProps={{ + bordered: true, + rowHeight: 40, + className: 'mlCopyToSpace__spacesList', + 'data-test-subj': 'mlFormSpaceSelector', + }} + searchable + > + {(list, search) => { + return ( + <> + {search} + {list} + + ); + }} + + + + + + + toggleShareOption(true)} + disabled={shareToAllSpaces.disabled} + /> + + + ); +}; + +function createLabel({ + title, + text, + disabled, + tooltip, +}: { + title: string; + text: string; + disabled: boolean; + tooltip?: string; +}) { + return ( + <> + + + {title} + + {tooltip && ( + + + + )} + + + + {text} + + + ); +} diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index f08ca3c153961..0f96c8f8282ef 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -10,3 +10,4 @@ export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; export { useMlUrlGenerator, useMlLink } from './use_create_url'; +export { useMlApiContext } from './use_ml_api_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts new file mode 100644 index 0000000000000..4f0d4f9cacf19 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMlKibana } from './kibana_context'; + +export const useMlApiContext = () => { + return useMlKibana().services.mlServices.mlApiServices; +}; diff --git a/x-pack/plugins/ml/public/application/contexts/spaces/index.ts b/x-pack/plugins/ml/public/application/contexts/spaces/index.ts new file mode 100644 index 0000000000000..dc68767052176 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/spaces/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + SpacesContext, + SpacesContextValue, + createSpacesContext, + useSpacesContext, +} from './spaces_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts new file mode 100644 index 0000000000000..d83273c6a9c89 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createContext, useContext } from 'react'; +import { HttpSetup } from 'src/core/public'; +import { SpacesManager, Space } from '../../../../../spaces/public'; + +export interface SpacesContextValue { + spacesManager: SpacesManager | null; + allSpaces: Space[]; + spacesEnabled: boolean; +} + +export const SpacesContext = createContext>({}); + +export function createSpacesContext(http: HttpSetup, spacesEnabled: boolean) { + return { + spacesManager: spacesEnabled ? new SpacesManager(http) : null, + allSpaces: [], + spacesEnabled, + } as SpacesContextValue; +} + +export function useSpacesContext() { + const context = useContext(SpacesContext); + + if (context.spacesManager === undefined) { + throw new Error('required attribute is undefined'); + } + + return context as SpacesContextValue; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 63b7074ec3aaa..f4cd64aa8c497 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -82,6 +82,7 @@ function getItemIdToExpandedRowMap( interface Props { isManagementTable?: boolean; isMlEnabledInSpace?: boolean; + spacesEnabled?: boolean; blockRefresh?: boolean; pageState: ListingPageUrlState; updatePageState: (update: Partial) => void; @@ -89,6 +90,7 @@ interface Props { export const DataFrameAnalyticsList: FC = ({ isManagementTable = false, isMlEnabledInSpace = true, + spacesEnabled = false, blockRefresh = false, pageState, updatePageState, @@ -159,7 +161,7 @@ export const DataFrameAnalyticsList: FC = ({ const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); // Subscribe to the refresh observable to trigger reloading the analytics list. - useRefreshAnalyticsList( + const { refresh } = useRefreshAnalyticsList( { isLoading: setIsLoading, onRefresh: getAnalyticsCallback, @@ -171,7 +173,9 @@ export const DataFrameAnalyticsList: FC = ({ expandedRowItemIds, setExpandedRowItemIds, isManagementTable, - isMlEnabledInSpace + isMlEnabledInSpace, + spacesEnabled, + refresh ); const { onTableChange, pagination, sorting } = useTableSettings( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 84c37ac8b816b..bf13471c0d18b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -112,7 +112,7 @@ export interface DataFrameAnalyticsListRow { mode: string; state: DataFrameAnalyticsStats['state']; stats: DataFrameAnalyticsStats; - spaces?: string[]; + spaceIds?: string[]; } // Used to pass on attribute names to table columns diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 93868ce0c17e6..69335b55f4c78 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -148,7 +148,9 @@ export const useColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], setExpandedRowItemIds: React.Dispatch>, isManagementTable: boolean = false, - isMlEnabledInSpace: boolean = true + isMlEnabledInSpace: boolean = true, + spacesEnabled: boolean = true, + refresh: () => void = () => {} ) => { const { actions, modals } = useActions(isManagementTable); function toggleDetails(item: DataFrameAnalyticsListRow) { @@ -278,16 +280,24 @@ export const useColumns = ( ]; if (isManagementTable === true) { - // insert before last column - columns.splice(columns.length - 1, 0, { - name: i18n.translate('xpack.ml.jobsList.analyticsSpacesLabel', { - defaultMessage: 'Spaces', - }), - render: (item: DataFrameAnalyticsListRow) => - Array.isArray(item.spaces) ? : null, - width: '75px', - }); - + if (spacesEnabled === true) { + // insert before last column + columns.splice(columns.length - 1, 0, { + name: i18n.translate('xpack.ml.jobsList.analyticsSpacesLabel', { + defaultMessage: 'Spaces', + }), + render: (item: DataFrameAnalyticsListRow) => + Array.isArray(item.spaceIds) ? ( + + ) : null, + width: '90px', + }); + } // Remove actions if Ml not enabled in current space if (isMlEnabledInSpace === false) { columns.pop(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index beb490d025785..2d251d94e9ca7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -155,7 +155,7 @@ export const getAnalyticsFactory = ( mode: DATA_FRAME_MODE.BATCH, state: stats.state, stats, - spaces: spaces[config.id] ?? [], + spaceIds: spaces[config.id] ?? [], }); return reducedtableRows; }, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 4213b0a6df1fb..9bbff4ea28539 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -95,7 +95,7 @@ export class JobsList extends Component { } render() { - const { loading, isManagementTable } = this.props; + const { loading, isManagementTable, spacesEnabled } = this.props; const selectionControls = { selectable: (job) => job.deleting !== true, selectableMessage: (selectable, rowItem) => @@ -242,13 +242,22 @@ export class JobsList extends Component { ]; if (isManagementTable === true) { - // insert before last column - columns.splice(columns.length - 1, 0, { - name: i18n.translate('xpack.ml.jobsList.spacesLabel', { - defaultMessage: 'Spaces', - }), - render: (item) => , - }); + if (spacesEnabled === true) { + // insert before last column + columns.splice(columns.length - 1, 0, { + name: i18n.translate('xpack.ml.jobsList.spacesLabel', { + defaultMessage: 'Spaces', + }), + render: (item) => ( + + ), + }); + } // Remove actions if Ml not enabled in current space if (this.props.isMlEnabledInSpace === false) { columns.pop(); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 570172abb28c1..6e3b9031de653 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -57,6 +57,7 @@ export class JobsListView extends Component { deletingJobIds: [], }; + this.spacesEnabled = props.spacesEnabled ?? false; this.updateFunctions = {}; this.showEditJobFlyout = () => {}; @@ -253,7 +254,7 @@ export class JobsListView extends Component { const expandedJobsIds = Object.keys(this.state.itemIdToExpandedRowMap); try { let spaces = {}; - if (this.props.isManagementTable) { + if (this.props.spacesEnabled && this.props.isManagementTable) { const allSpaces = await ml.savedObjects.jobsSpaces(); spaces = allSpaces['anomaly-detector']; } @@ -266,8 +267,11 @@ export class JobsListView extends Component { delete job.fullJob; } job.latestTimestampSortValue = job.latestTimestampMs || 0; - job.spaces = - this.props.isManagementTable && spaces && spaces[job.id] !== undefined + job.spaceIds = + this.props.spacesEnabled && + this.props.isManagementTable && + spaces && + spaces[job.id] !== undefined ? spaces[job.id] : []; return job; @@ -379,8 +383,10 @@ export class JobsListView extends Component { loading={loading} isManagementTable={true} isMlEnabledInSpace={this.props.isMlEnabledInSpace} + spacesEnabled={this.props.spacesEnabled} jobsViewState={this.props.jobsViewState} onJobsViewStateUpdate={this.props.onJobsViewStateUpdate} + refreshJobs={() => this.refreshJobSummaryList(true)} /> diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 1089484449bab..8ad18e2b821b6 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -19,8 +19,11 @@ import { EuiTabbedContent, EuiText, EuiTitle, + EuiTabbedContentTab, } from '@elastic/eui'; +import { PLUGIN_ID } from '../../../../../../common/constants/app'; +import { createSpacesContext, SpacesContext } from '../../../../contexts/spaces'; import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/'; import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; @@ -35,16 +38,15 @@ import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_vi import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; +import { SpacesPluginStart } from '../../../../../../../spaces/public'; +import { JobSpacesRepairFlyout } from '../../../../components/job_spaces_repair'; import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs'; import { getMlGlobalServices } from '../../../../app'; import { ListingPageUrlState } from '../../../../../../common/types/common'; import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page'; -interface Tab { +interface Tab extends EuiTabbedContentTab { 'data-test-subj': string; - id: string; - name: string; - content: any; } function usePageState( @@ -65,7 +67,7 @@ function usePageState( return [pageState, updateState]; } -function useTabs(isMlEnabledInSpace: boolean): Tab[] { +function useTabs(isMlEnabledInSpace: boolean, spacesEnabled: boolean): Tab[] { const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState()); const [dfaPageState, updateDfaPageState] = usePageState(getDefaultDFAListState()); @@ -85,6 +87,7 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { onJobsViewStateUpdate={updateAdPageState} isManagementTable={true} isMlEnabledInSpace={isMlEnabledInSpace} + spacesEnabled={spacesEnabled} /> ), @@ -101,6 +104,7 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { @@ -116,18 +120,28 @@ export const JobsListPage: FC<{ coreStart: CoreStart; share: SharePluginStart; history: ManagementAppMountParams['history']; -}> = ({ coreStart, share, history }) => { + spaces?: SpacesPluginStart; +}> = ({ coreStart, share, history, spaces }) => { + const spacesEnabled = spaces !== undefined; const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); + const [showRepairFlyout, setShowRepairFlyout] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); - const tabs = useTabs(isMlEnabledInSpace); + const tabs = useTabs(isMlEnabledInSpace, spacesEnabled); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); const I18nContext = coreStart.i18n.Context; + const spacesContext = useMemo(() => createSpacesContext(coreStart.http, spacesEnabled), []); const check = async () => { try { - const checkPrivilege = await checkGetManagementMlJobsResolver(); - setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); + const { mlFeatureEnabledInSpace } = await checkGetManagementMlJobsResolver(); + setIsMlEnabledInSpace(mlFeatureEnabledInSpace); + spacesContext.spacesEnabled = spacesEnabled; + if (spacesEnabled && spacesContext.spacesManager !== null) { + spacesContext.allSpaces = (await spacesContext.spacesManager.getSpaces()).filter( + (space) => space.disabledFeatures.includes(PLUGIN_ID) === false + ); + } } catch (e) { setAccessDenied(true); } @@ -170,6 +184,10 @@ export const JobsListPage: FC<{ ); } + function onCloseRepairFlyout() { + setShowRepairFlyout(false); + } + if (accessDenied) { return ; } @@ -180,51 +198,66 @@ export const JobsListPage: FC<{ - - - - - -

- {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { - defaultMessage: 'Machine Learning Jobs', - })} -

-
- - - {currentTabId === 'anomaly_detection_jobs' - ? anomalyDetectionDocsLabel - : analyticsDocsLabel} - - -
-
- - - - {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { - defaultMessage: 'View machine learning analytics and anomaly detection jobs.', - })} - - - - {renderTabs()} -
-
+ + + + + + +

+ {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { + defaultMessage: 'Machine Learning Jobs', + })} +

+
+ + + {currentTabId === 'anomaly_detection_jobs' + ? anomalyDetectionDocsLabel + : analyticsDocsLabel} + + +
+
+ + + + {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { + defaultMessage: 'View machine learning analytics and anomaly detection jobs.', + })} + + + + + {spacesEnabled && ( + <> + setShowRepairFlyout(true)}> + {i18n.translate('xpack.ml.management.jobsList.repairFlyoutButton', { + defaultMessage: 'Repair saved objects', + })} + + {showRepairFlyout && } + + + )} + {renderTabs()} + +
+
+
diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts index 422121e1845b2..284220e4e3caf 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts +++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts @@ -14,14 +14,19 @@ import { getJobsListBreadcrumbs } from '../breadcrumbs'; import { setDependencyCache, clearCache } from '../../util/dependency_cache'; import './_index.scss'; import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; +import { SpacesPluginStart } from '../../../../../spaces/public'; const renderApp = ( element: HTMLElement, history: ManagementAppMountParams['history'], coreStart: CoreStart, - share: SharePluginStart + share: SharePluginStart, + spaces?: SpacesPluginStart ) => { - ReactDOM.render(React.createElement(JobsListPage, { coreStart, history, share }), element); + ReactDOM.render( + React.createElement(JobsListPage, { coreStart, history, share, spaces }), + element + ); return () => { unmountComponentAtNode(element); clearCache(); @@ -42,6 +47,11 @@ export async function mountApp( }); params.setBreadcrumbs(getJobsListBreadcrumbs()); - - return renderApp(params.element, params.history, coreStart, pluginsStart.share); + return renderApp( + params.element, + params.history, + coreStart, + pluginsStart.share, + pluginsStart.spaces + ); } diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts index a1323b39b3bcc..b47cf3f62871c 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts @@ -9,18 +9,23 @@ import { HttpService } from '../http_service'; import { basePath } from './index'; -import { JobType } from '../../../../common/types/saved_objects'; +import { + JobType, + RepairSavedObjectResponse, + SavedObjectResult, + JobsSpacesResponse, +} from '../../../../common/types/saved_objects'; export const savedObjectsApiProvider = (httpService: HttpService) => ({ jobsSpaces() { - return httpService.http({ + return httpService.http({ path: `${basePath()}/saved_objects/jobs_spaces`, method: 'GET', }); }, assignJobToSpace(jobType: JobType, jobIds: string[], spaces: string[]) { const body = JSON.stringify({ jobType, jobIds, spaces }); - return httpService.http({ + return httpService.http({ path: `${basePath()}/saved_objects/assign_job_to_space`, method: 'POST', body, @@ -28,10 +33,18 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({ }, removeJobFromSpace(jobType: JobType, jobIds: string[], spaces: string[]) { const body = JSON.stringify({ jobType, jobIds, spaces }); - return httpService.http({ + return httpService.http({ path: `${basePath()}/saved_objects/remove_job_from_space`, method: 'POST', body, }); }, + + repairSavedObjects(simulate: boolean = false) { + return httpService.http({ + path: `${basePath()}/saved_objects/repair`, + method: 'GET', + query: { simulate }, + }); + }, }); diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 8a25c1c49e255..1cc69ac2239ab 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -26,6 +26,7 @@ import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; import type { EmbeddableSetup } from 'src/plugins/embeddable/public'; +import type { SpacesPluginStart } from '../../spaces/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import type { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; @@ -50,6 +51,7 @@ export interface MlStartDependencies { share: SharePluginStart; kibanaLegacy: KibanaLegacyStart; uiActions: UiActionsStart; + spaces?: SpacesPluginStart; } export interface MlSetupDependencies { security?: SecurityPluginSetup; diff --git a/x-pack/plugins/ml/server/saved_objects/repair.ts b/x-pack/plugins/ml/server/saved_objects/repair.ts index 1b0b4b2609a91..692217e5fac36 100644 --- a/x-pack/plugins/ml/server/saved_objects/repair.ts +++ b/x-pack/plugins/ml/server/saved_objects/repair.ts @@ -7,8 +7,13 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import type { JobObject, JobSavedObjectService } from './service'; -import { JobType, RepairSavedObjectResponse } from '../../common/types/saved_objects'; +import { + JobType, + RepairSavedObjectResponse, + InitializeSavedObjectResponse, +} from '../../common/types/saved_objects'; import { checksFactory } from './checks'; +import { getSavedObjectClientError } from './util'; import { Datafeed } from '../../common/types/anomaly_detection_jobs'; @@ -54,7 +59,7 @@ export function repairFactory( } catch (error) { results.savedObjectsCreated[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -75,7 +80,7 @@ export function repairFactory( } catch (error) { results.savedObjectsCreated[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -97,7 +102,7 @@ export function repairFactory( } catch (error) { results.savedObjectsDeleted[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -118,7 +123,7 @@ export function repairFactory( } catch (error) { results.savedObjectsDeleted[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -143,7 +148,10 @@ export function repairFactory( } results.datafeedsAdded[job.jobId] = { success: true }; } catch (error) { - results.datafeedsAdded[job.jobId] = { success: false, error }; + results.datafeedsAdded[job.jobId] = { + success: false, + error: getSavedObjectClientError(error), + }; } }); } @@ -163,7 +171,10 @@ export function repairFactory( await jobSavedObjectService.deleteDatafeed(datafeedId); results.datafeedsRemoved[job.jobId] = { success: true }; } catch (error) { - results.datafeedsRemoved[job.jobId] = { success: false, error: error.body ?? error }; + results.datafeedsRemoved[job.jobId] = { + success: false, + error: getSavedObjectClientError(error), + }; } }); } @@ -173,8 +184,11 @@ export function repairFactory( return results; } - async function initSavedObjects(simulate: boolean = false, spaceOverrides?: JobSpaceOverrides) { - const results: { jobs: Array<{ id: string; type: string }>; success: boolean; error?: any } = { + async function initSavedObjects( + simulate: boolean = false, + spaceOverrides?: JobSpaceOverrides + ): Promise { + const results: InitializeSavedObjectResponse = { jobs: [], success: true, }; @@ -211,7 +225,6 @@ export function repairFactory( type: attributes.type, }); }); - return { jobs: jobs.map((j) => j.job.job_id) }; } catch (error) { results.success = false; results.error = Boom.boomify(error).output; diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index 1193dfde85f1c..ecaf0869d196c 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -9,6 +9,7 @@ import { KibanaRequest, SavedObjectsClientContract, SavedObjectsFindOptions } fr import type { SecurityPluginSetup } from '../../../security/server'; import { JobType, ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; import { MLJobNotFound } from '../lib/ml_client'; +import { getSavedObjectClientError } from './util'; import { authorizationProvider } from './authorization'; export interface JobObject { @@ -61,14 +62,24 @@ export function jobSavedObjectServiceFactory( async function _createJob(jobType: JobType, jobId: string, datafeedId?: string) { await isMlReady(); + const job: JobObject = { job_id: jobId, datafeed_id: datafeedId ?? null, type: jobType, }; + + const id = savedObjectId(job); + + try { + await savedObjectsClient.delete(ML_SAVED_OBJECT_TYPE, id, { force: true }); + } catch (error) { + // the saved object may exist if a previous job with the same ID has been deleted. + // if not, this error will be throw which we ignore. + } + await savedObjectsClient.create(ML_SAVED_OBJECT_TYPE, job, { - id: savedObjectId(job), - overwrite: true, + id, }); } @@ -257,7 +268,7 @@ export function jobSavedObjectServiceFactory( } catch (error) { results[id] = { success: false, - error, + error: getSavedObjectClientError(error), }; } } @@ -278,7 +289,7 @@ export function jobSavedObjectServiceFactory( } catch (error) { results[job.attributes.job_id] = { success: false, - error, + error: getSavedObjectClientError(error), }; } } diff --git a/x-pack/plugins/ml/server/saved_objects/util.ts b/x-pack/plugins/ml/server/saved_objects/util.ts index 72eca6ff5977a..4349c216abffa 100644 --- a/x-pack/plugins/ml/server/saved_objects/util.ts +++ b/x-pack/plugins/ml/server/saved_objects/util.ts @@ -35,3 +35,7 @@ export function savedObjectClientsFactory( }, }; } + +export function getSavedObjectClientError(error: any) { + return error.isBoom && error.output?.payload ? error.output.payload : error.body ?? error; +} diff --git a/x-pack/plugins/spaces/public/index.ts b/x-pack/plugins/spaces/public/index.ts index ecbf1d8b36b7d..5fc56dfb7a295 100644 --- a/x-pack/plugins/spaces/public/index.ts +++ b/x-pack/plugins/spaces/public/index.ts @@ -14,6 +14,8 @@ export { SpaceAvatar, getSpaceColor, getSpaceImageUrl, getSpaceInitials } from ' export { SpacesPluginSetup, SpacesPluginStart } from './plugin'; +export { SpacesManager } from './spaces_manager'; + export const plugin = () => { return new SpacesPlugin(); };