diff --git a/.eslintrc.js b/.eslintrc.js index 9b00135df5bac..087d6276cd33f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -82,12 +82,6 @@ module.exports = { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['src/legacy/core_plugins/vis_type_table/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: [ 'src/legacy/core_plugins/vis_default_editor/public/components/controls/**/*.{ts,tsx}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 51433f598ac16..b924c7a1a2c29 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -179,6 +179,8 @@ /x-pack/legacy/plugins/rollup/ @elastic/es-ui /x-pack/plugins/searchprofiler/ @elastic/es-ui /x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui +/x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui +/x-pack/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/watcher/ @elastic/es-ui # Endpoint diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c1f06aff722b5..2d4d00b730109 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -231,7 +231,7 @@ This setting does not impact <> and <> visualizations. Supported on {ece}. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the -https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system] +https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] and only include polygons. If the file is hosted on a separate domain from Kibana, the server needs to be CORS-enabled so Kibana can download the file. The following example shows a valid regionmap configuration. diff --git a/examples/demo_search/common/index.ts b/examples/demo_search/common/index.ts index 6587ee96ef61b..8ea8d6186ee82 100644 --- a/examples/demo_search/common/index.ts +++ b/examples/demo_search/common/index.ts @@ -20,6 +20,7 @@ import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../src/plugins/data/public'; export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY'; +export const ASYNC_DEMO_SEARCH_STRATEGY = 'ASYNC_DEMO_SEARCH_STRATEGY'; export interface IDemoRequest extends IKibanaSearchRequest { mood: string | 'sad' | 'happy'; @@ -29,3 +30,11 @@ export interface IDemoRequest extends IKibanaSearchRequest { export interface IDemoResponse extends IKibanaSearchResponse { greeting: string; } + +export interface IAsyncDemoRequest extends IKibanaSearchRequest { + fibonacciNumbers: number; +} + +export interface IAsyncDemoResponse extends IKibanaSearchResponse { + fibonacciSequence: number[]; +} diff --git a/examples/demo_search/kibana.json b/examples/demo_search/kibana.json index 0603706b07d1f..cb73274ed23f7 100644 --- a/examples/demo_search/kibana.json +++ b/examples/demo_search/kibana.json @@ -2,7 +2,7 @@ "id": "demoSearch", "version": "0.0.1", "kibanaVersion": "kibana", - "configPath": ["demo_search"], + "configPath": ["demoSearch"], "server": true, "ui": true, "requiredPlugins": ["data"], diff --git a/examples/demo_search/public/async_demo_search_strategy.ts b/examples/demo_search/public/async_demo_search_strategy.ts new file mode 100644 index 0000000000000..7a3f33ce05a75 --- /dev/null +++ b/examples/demo_search/public/async_demo_search_strategy.ts @@ -0,0 +1,69 @@ +/* + * 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 { Observable } from 'rxjs'; +import { + ISearchContext, + TSearchStrategyProvider, + ISearchStrategy, +} from '../../../src/plugins/data/public'; + +import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoResponse } from '../common'; +import { ASYNC_SEARCH_STRATEGY } from '../../../x-pack/plugins/data_enhanced/public'; + +/** + * This demo search strategy provider simply provides a shortcut for calling the DEMO_ASYNC_SEARCH_STRATEGY + * on the server side, without users having to pass it in explicitly, and it takes advantage of the + * already registered ASYNC_SEARCH_STRATEGY that exists on the client. + * + * so instead of callers having to do: + * + * ``` + * search( + * { ...request, serverStrategy: DEMO_ASYNC_SEARCH_STRATEGY }, + * options, + * ASYNC_SEARCH_STRATEGY + * ) as Observable, + *``` + + * They can instead just do + * + * ``` + * search(request, options, DEMO_ASYNC_SEARCH_STRATEGY); + * ``` + * + * and are ensured type safety in regard to the request and response objects. + * + * @param context - context supplied by other plugins. + * @param search - a search function to access other strategies that have already been registered. + */ +export const asyncDemoClientSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext +): ISearchStrategy => { + const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY); + const { search } = asyncStrategyProvider(context); + return { + search: (request, options) => { + return search( + { ...request, serverStrategy: ASYNC_DEMO_SEARCH_STRATEGY }, + options + ) as Observable; + }, + }; +}; diff --git a/examples/demo_search/public/demo_search_strategy.ts b/examples/demo_search/public/demo_search_strategy.ts index cb2480c8a5f19..8dc2779a8544c 100644 --- a/examples/demo_search/public/demo_search_strategy.ts +++ b/examples/demo_search/public/demo_search_strategy.ts @@ -43,7 +43,7 @@ import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common'; * ``` * context.search(request, options, DEMO_SEARCH_STRATEGY); * ``` - * + * * and are ensured type safety in regard to the request and response objects. * * @param context - context supplied by other plugins. diff --git a/examples/demo_search/public/plugin.ts b/examples/demo_search/public/plugin.ts index 62c912716e627..a2539cc7a21c5 100644 --- a/examples/demo_search/public/plugin.ts +++ b/examples/demo_search/public/plugin.ts @@ -19,9 +19,16 @@ import { DataPublicPluginSetup } from '../../../src/plugins/data/public'; import { Plugin, CoreSetup } from '../../../src/core/public'; -import { DEMO_SEARCH_STRATEGY } from '../common'; +import { + DEMO_SEARCH_STRATEGY, + IDemoRequest, + IDemoResponse, + ASYNC_DEMO_SEARCH_STRATEGY, + IAsyncDemoRequest, + IAsyncDemoResponse, +} from '../common'; import { demoClientSearchStrategyProvider } from './demo_search_strategy'; -import { IDemoRequest, IDemoResponse } from '../common'; +import { asyncDemoClientSearchStrategyProvider } from './async_demo_search_strategy'; interface DemoDataSearchSetupDependencies { data: DataPublicPluginSetup; @@ -39,10 +46,12 @@ interface DemoDataSearchSetupDependencies { declare module '../../../src/plugins/data/public' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest; } export interface IResponseTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoResponse; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse; } } @@ -52,6 +61,10 @@ export class DemoDataPlugin implements Plugin { DEMO_SEARCH_STRATEGY, demoClientSearchStrategyProvider ); + deps.data.search.registerSearchStrategyProvider( + ASYNC_DEMO_SEARCH_STRATEGY, + asyncDemoClientSearchStrategyProvider + ); } public start() {} diff --git a/examples/demo_search/server/async_demo_search_strategy.ts b/examples/demo_search/server/async_demo_search_strategy.ts new file mode 100644 index 0000000000000..d2d40891a5d93 --- /dev/null +++ b/examples/demo_search/server/async_demo_search_strategy.ts @@ -0,0 +1,60 @@ +/* + * 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 { TSearchStrategyProvider } from '../../../src/plugins/data/server'; +import { ASYNC_DEMO_SEARCH_STRATEGY } from '../common'; + +function getFibonacciSequence(n = 0) { + const beginning = [0, 1].slice(0, n); + return Array(Math.max(0, n)) + .fill(null) + .reduce((sequence, value, i) => { + if (i < 2) return sequence; + return [...sequence, sequence[i - 1] + sequence[i - 2]]; + }, beginning); +} + +const generateId = (() => { + let id = 0; + return () => `${id++}`; +})(); + +const loadedMap = new Map(); +const totalMap = new Map(); + +export const asyncDemoSearchStrategyProvider: TSearchStrategyProvider = () => { + return { + search: async request => { + const id = request.id ?? generateId(); + + const loaded = (loadedMap.get(id) ?? 0) + 1; + loadedMap.set(id, loaded); + + const total = request.fibonacciNumbers ?? totalMap.get(id); + totalMap.set(id, total); + + const fibonacciSequence = getFibonacciSequence(loaded); + return { id, total, loaded, fibonacciSequence }; + }, + cancel: async id => { + loadedMap.delete(id); + totalMap.delete(id); + }, + }; +}; diff --git a/examples/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts index 653aa217717fa..49fbae43e3aa2 100644 --- a/examples/demo_search/server/plugin.ts +++ b/examples/demo_search/server/plugin.ts @@ -20,7 +20,15 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; import { demoSearchStrategyProvider } from './demo_search_strategy'; -import { DEMO_SEARCH_STRATEGY, IDemoRequest, IDemoResponse } from '../common'; +import { + DEMO_SEARCH_STRATEGY, + IDemoRequest, + IDemoResponse, + ASYNC_DEMO_SEARCH_STRATEGY, + IAsyncDemoRequest, + IAsyncDemoResponse, +} from '../common'; +import { asyncDemoSearchStrategyProvider } from './async_demo_search_strategy'; interface IDemoSearchExplorerDeps { data: DataPluginSetup; @@ -38,10 +46,12 @@ interface IDemoSearchExplorerDeps { declare module '../../../src/plugins/data/server' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoRequest; } export interface IResponseTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoResponse; + [ASYNC_DEMO_SEARCH_STRATEGY]: IAsyncDemoResponse; } } @@ -54,6 +64,11 @@ export class DemoDataPlugin implements Plugin id: 'demoSearch', component: , }, + { + title: 'Async demo search strategy', + id: 'asyncDemoSearch', + component: , + }, ]; const routes = pages.map((page, i) => ( diff --git a/examples/search_explorer/public/async_demo_strategy.tsx b/examples/search_explorer/public/async_demo_strategy.tsx new file mode 100644 index 0000000000000..40ddcc1f48fe7 --- /dev/null +++ b/examples/search_explorer/public/async_demo_strategy.tsx @@ -0,0 +1,123 @@ +/* + * 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 React from 'react'; +import { + EuiPageContentBody, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, +} from '@elastic/eui'; +import { ISearchGeneric } from '../../../src/plugins/data/public'; +import { DoSearch } from './do_search'; +import { GuideSection } from './guide_section'; + +import { ASYNC_DEMO_SEARCH_STRATEGY, IAsyncDemoRequest } from '../../demo_search/common'; + +// @ts-ignore +import demoStrategyServerProvider from '!!raw-loader!./../../demo_search/server/async_demo_search_strategy'; +// @ts-ignore +import demoStrategyPublicProvider from '!!raw-loader!./../../demo_search/public/async_demo_search_strategy'; +// @ts-ignore +import demoStrategyServerPlugin from '!!raw-loader!./../../demo_search/server/plugin'; +// @ts-ignore +import demoStrategyPublicPlugin from '!!raw-loader!./../../demo_search/public/plugin'; + +interface Props { + search: ISearchGeneric; +} + +interface State { + searching: boolean; + fibonacciNumbers: number; + changes: boolean; + error?: any; +} + +export class AsyncDemoStrategy extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + searching: false, + changes: false, + fibonacciNumbers: 5, + }; + } + + renderDemo = () => { + const request: IAsyncDemoRequest = { + fibonacciNumbers: this.state.fibonacciNumbers, + }; + return ( + + + + + this.setState({ fibonacciNumbers: parseFloat(e.target.value) })} + /> + + + + + this.props.search(request, { signal }, ASYNC_DEMO_SEARCH_STRATEGY) + } + /> + + ); + }; + + render() { + return ( + + + + ); + } +} diff --git a/examples/search_explorer/public/do_search.tsx b/examples/search_explorer/public/do_search.tsx index f279b9fcd6e23..a6b6b9b57db4a 100644 --- a/examples/search_explorer/public/do_search.tsx +++ b/examples/search_explorer/public/do_search.tsx @@ -118,8 +118,8 @@ ${requestStr} Response: extends IndexOptions { id: string; } +interface MigrateResponse { + success: boolean; + result: Array<{ status: string }>; +} + export class KbnClientSavedObjects { constructor(private readonly log: ToolingLog, private readonly requester: KbnClientRequester) {} + /** + * Run the saved objects migration + */ + public async migrate() { + this.log.debug('Migrating saved objects'); + + return await this.requester.request({ + description: 'migrate saved objects', + path: uriencode`/internal/saved_objects/_migrate`, + method: 'POST', + body: {}, + }); + } + /** * Get an object */ diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts new file mode 100644 index 0000000000000..7750f9145667e --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -0,0 +1,208 @@ +/* + * 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 Path from 'path'; + +import jestDiff from 'jest-diff'; +import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { reformatJestDiff, getOptimizerCacheKey, diffCacheKey } from './cache_keys'; +import { OptimizerConfig } from './optimizer_config'; + +jest.mock('./get_changes.ts', () => ({ + getChanges: async () => + new Map([ + ['/foo/bar/a', 'modified'], + ['/foo/bar/b', 'modified'], + ['/foo/bar/c', 'deleted'], + ]), +})); + +jest.mock('./get_mtimes.ts', () => ({ + getMtimes: async (paths: string[]) => new Map(paths.map(path => [path, 12345])), +})); + +jest.mock('execa'); + +jest.mock('fs', () => { + const realFs = jest.requireActual('fs'); + return { + ...realFs, + readFile: jest.fn(realFs.readFile), + }; +}); + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { + expect(cmd).toBe('git'); + expect(args).toEqual([ + 'log', + '-n', + '1', + '--pretty=format:%H', + '--', + expect.stringContaining('kbn-optimizer'), + ]); + expect(opts).toEqual({ + cwd: REPO_ROOT, + }); + + return { + stdout: '', + }; +}); + +describe('getOptimizerCacheKey()', () => { + it('uses latest commit, bootstrap cache, and changed files to create unique value', async () => { + jest + .requireMock('fs') + .readFile.mockImplementation( + (path: string, enc: string, cb: (err: null, file: string) => void) => { + expect(path).toBe( + Path.resolve(REPO_ROOT, 'packages/kbn-optimizer/target/.bootstrap-cache') + ); + expect(enc).toBe('utf8'); + cb(null, ''); + } + ); + + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + }); + + await expect(getOptimizerCacheKey(config)).resolves.toMatchInlineSnapshot(` + Object { + "bootstrap": "", + "deletedPaths": Array [ + "/foo/bar/c", + ], + "lastCommit": "", + "modifiedTimes": Object { + "/foo/bar/a": 12345, + "/foo/bar/b": 12345, + }, + "workerConfig": Object { + "browserslistEnv": "dev", + "cache": true, + "dist": false, + "optimizerCacheKey": "♻", + "profileWebpack": false, + "repoRoot": , + "watch": false, + }, + } + `); + }); +}); + +describe('diffCacheKey()', () => { + it('returns undefined if values are equal', () => { + expect(diffCacheKey('1', '1')).toBe(undefined); + expect(diffCacheKey(1, 1)).toBe(undefined); + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { a: 'b' }])).toBe(undefined); + expect( + diffCacheKey( + { + a: '1', + b: '2', + }, + { + b: '2', + a: '1', + } + ) + ).toBe(undefined); + }); + + it('returns a diff if the values are different', () => { + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { b: 'a' }])).toMatchInlineSnapshot(` + "- Expected + + Received + +  Array [ +  \\"1\\", +  \\"2\\", +  Object { + - \\"a\\": \\"b\\", + + \\"b\\": \\"a\\", +  }, +  ]" + `); + expect( + diffCacheKey( + { + a: '1', + b: '1', + }, + { + b: '2', + a: '2', + } + ) + ).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": \\"1\\", + - \\"b\\": \\"1\\", + + \\"a\\": \\"2\\", + + \\"b\\": \\"2\\", +  }" + `); + }); +}); + +describe('reformatJestDiff()', () => { + it('reformats large jestDiff output to focus on the changed lines', () => { + const diff = jestDiff( + { + a: ['1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1', '1', '1', '1', '1', '1'], + }, + { + b: ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1'], + } + ); + + expect(reformatJestDiff(diff)).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": Array [ + + \\"b\\": Array [ +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + - \\"2\\", +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + + \\"2\\", +  \\"1\\", +  \\"1\\", +  ..." + `); + }); +}); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 451db9750ada7..fe0491870e4bd 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -43784,6 +43784,18 @@ class KbnClientSavedObjects { this.log = log; this.requester = requester; } + /** + * Run the saved objects migration + */ + async migrate() { + this.log.debug('Migrating saved objects'); + return await this.requester.request({ + description: 'migrate saved objects', + path: kbn_client_requester_1.uriencode `/internal/saved_objects/_migrate`, + method: 'POST', + body: {}, + }); + } /** * Get an object */ diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts index 821a10353f8ec..9d220cfdf94b7 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts @@ -21,6 +21,11 @@ import { IndexMapping } from './../../mappings'; import { buildActiveMappings, diffMappings } from './build_active_mappings'; describe('buildActiveMappings', () => { + test('creates a strict mapping', () => { + const mappings = buildActiveMappings({}); + expect(mappings.dynamic).toEqual('strict'); + }); + test('combines all mappings and includes core mappings', () => { const properties = { aaa: { type: 'text' }, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 494f834717def..bc29061b380b8 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -88,15 +88,27 @@ export class KibanaMigrator { } /** - * Migrates the mappings and documents in the Kibana index. This will run only + * Migrates the mappings and documents in the Kibana index. By default, this will run only * once and subsequent calls will return the result of the original call. * + * @param rerun - If true, method will run a new migration when called again instead of + * returning the result of the initial migration. This should only be used when factors external + * to Kibana itself alter the kibana index causing the saved objects mappings or data to change + * after the Kibana server performed the initial migration. + * + * @remarks When the `rerun` parameter is set to true, no checks are performed to ensure that no migration + * is currently running. Chained or concurrent calls to `runMigrations({ rerun: true })` can lead to + * multiple migrations running at the same time. When calling with this parameter, it's expected that the calling + * code should ensure that the initial call resolves before calling the function again. + * * @returns - A promise which resolves once all migrations have been applied. * The promise resolves with an array of migration statuses, one for each * elasticsearch index which was migrated. */ - public runMigrations(): Promise> { - if (this.migrationResult === undefined) { + public runMigrations({ rerun = false }: { rerun?: boolean } = {}): Promise< + Array<{ status: string }> + > { + if (this.migrationResult === undefined || rerun) { this.migrationResult = this.runMigrationsInternal(); } diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index f2f57798dd5f0..0afa24b18760b 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -20,6 +20,7 @@ import { InternalHttpServiceSetup } from '../../http'; import { Logger } from '../../logging'; import { SavedObjectConfig } from '../saved_objects_config'; +import { IKibanaMigrator } from '../migrations'; import { registerGetRoute } from './get'; import { registerCreateRoute } from './create'; import { registerDeleteRoute } from './delete'; @@ -32,17 +33,20 @@ import { registerLogLegacyImportRoute } from './log_legacy_import'; import { registerExportRoute } from './export'; import { registerImportRoute } from './import'; import { registerResolveImportErrorsRoute } from './resolve_import_errors'; +import { registerMigrateRoute } from './migrate'; export function registerRoutes({ http, logger, config, importableExportableTypes, + migratorPromise, }: { http: InternalHttpServiceSetup; logger: Logger; config: SavedObjectConfig; importableExportableTypes: string[]; + migratorPromise: Promise; }) { const router = http.createRouter('/api/saved_objects/'); @@ -58,4 +62,8 @@ export function registerRoutes({ registerExportRoute(router, config, importableExportableTypes); registerImportRoute(router, config, importableExportableTypes); registerResolveImportErrorsRoute(router, config, importableExportableTypes); + + const internalRouter = http.createRouter('/internal/saved_objects/'); + + registerMigrateRoute(internalRouter, migratorPromise); } diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 2c8d568b750c6..954e6d9e4831a 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -32,7 +32,7 @@ const config = { maxImportExportSize: 10000, } as SavedObjectConfig; -describe('POST /api/saved_objects/_import', () => { +describe('POST /internal/saved_objects/_import', () => { let server: setupServerReturn['server']; let httpSetup: setupServerReturn['httpSetup']; let handlerContext: setupServerReturn['handlerContext']; @@ -51,7 +51,7 @@ describe('POST /api/saved_objects/_import', () => { savedObjectsClient.find.mockResolvedValue(emptyResponse); - const router = httpSetup.createRouter('/api/saved_objects/'); + const router = httpSetup.createRouter('/internal/saved_objects/'); registerImportRoute(router, config, allowedTypes); await server.start(); @@ -63,7 +63,7 @@ describe('POST /api/saved_objects/_import', () => { it('formats successful response', async () => { const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_import') + .post('/internal/saved_objects/_import') .set('content-Type', 'multipart/form-data; boundary=BOUNDARY') .send( [ @@ -99,7 +99,7 @@ describe('POST /api/saved_objects/_import', () => { }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_import') + .post('/internal/saved_objects/_import') .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -148,7 +148,7 @@ describe('POST /api/saved_objects/_import', () => { }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_import') + .post('/internal/saved_objects/_import') .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -199,7 +199,7 @@ describe('POST /api/saved_objects/_import', () => { }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_import') + .post('/internal/saved_objects/_import') .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ @@ -249,7 +249,7 @@ describe('POST /api/saved_objects/_import', () => { }); const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_import') + .post('/internal/saved_objects/_import') .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') .send( [ diff --git a/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts b/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts new file mode 100644 index 0000000000000..870d50426904f --- /dev/null +++ b/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.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. + */ + +import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; + +export const migratorInstanceMock = mockKibanaMigrator.create(); +export const KibanaMigratorMock = jest.fn().mockImplementation(() => migratorInstanceMock); +jest.doMock('../../migrations/kibana/kibana_migrator', () => ({ + KibanaMigrator: KibanaMigratorMock, +})); diff --git a/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts new file mode 100644 index 0000000000000..928d17e7e5be2 --- /dev/null +++ b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { migratorInstanceMock } from './migrate.test.mocks'; +import * as kbnTestServer from '../../../../../test_utils/kbn_server'; + +describe('SavedObjects /_migrate endpoint', () => { + let root: ReturnType; + + beforeEach(async () => { + root = kbnTestServer.createRoot({ migrations: { skip: true } }); + await root.setup(); + await root.start(); + migratorInstanceMock.runMigrations.mockClear(); + }, 30000); + + afterEach(async () => { + await root.shutdown(); + }); + + it('calls runMigrations on the migrator with rerun=true when accessed', async () => { + await kbnTestServer.request + .post(root, '/internal/saved_objects/_migrate') + .send({}) + .expect(200); + + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith({ rerun: true }); + }); + + it('calls runMigrations multiple time when multiple access', async () => { + await kbnTestServer.request + .post(root, '/internal/saved_objects/_migrate') + .send({}) + .expect(200); + + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); + + await kbnTestServer.request + .post(root, '/internal/saved_objects/_migrate') + .send({}) + .expect(200); + + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/core/server/saved_objects/routes/migrate.ts b/src/core/server/saved_objects/routes/migrate.ts new file mode 100644 index 0000000000000..69e99d10acd09 --- /dev/null +++ b/src/core/server/saved_objects/routes/migrate.ts @@ -0,0 +1,45 @@ +/* + * 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 { IRouter } from '../../http'; +import { IKibanaMigrator } from '../migrations'; + +export const registerMigrateRoute = ( + router: IRouter, + migratorPromise: Promise +) => { + router.post( + { + path: '/_migrate', + validate: false, + options: { + tags: ['access:migrateSavedObjects'], + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const migrator = await migratorPromise; + await migrator.runMigrations({ rerun: true }); + return res.ok({ + body: { + success: true, + }, + }); + }) + ); +}; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index ece00539536e1..fa2b67a3e43b2 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -17,8 +17,9 @@ * under the License. */ -import { CoreService } from 'src/core/types'; +import { Subject } from 'rxjs'; import { first, filter, take } from 'rxjs/operators'; +import { CoreService } from '../../types'; import { SavedObjectsClient, SavedObjectsClientProvider, @@ -36,7 +37,7 @@ import { SavedObjectsMigrationConfigType, SavedObjectConfig, } from './saved_objects_config'; -import { InternalHttpServiceSetup, KibanaRequest } from '../http'; +import { KibanaRequest, InternalHttpServiceSetup } from '../http'; import { SavedObjectsClientContract, SavedObjectsType, SavedObjectsLegacyUiExports } from './types'; import { ISavedObjectsRepository, SavedObjectsRepository } from './service/lib/repository'; import { @@ -47,8 +48,8 @@ import { Logger } from '../logging'; import { convertLegacyTypes } from './utils'; import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; import { PropertyValidators } from './validation'; -import { registerRoutes } from './routes'; import { SavedObjectsSerializer } from './serialization'; +import { registerRoutes } from './routes'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to @@ -201,9 +202,9 @@ export interface SavedObjectsRepositoryFactory { /** @internal */ export interface SavedObjectsSetupDeps { + http: InternalHttpServiceSetup; legacyPlugins: LegacyServiceDiscoverPlugins; elasticsearch: InternalElasticsearchServiceSetup; - http: InternalHttpServiceSetup; } interface WrappedClientFactoryWrapper { @@ -225,6 +226,7 @@ export class SavedObjectsService private clientFactoryProvider?: SavedObjectsClientFactoryProvider; private clientFactoryWrappers: WrappedClientFactoryWrapper[] = []; + private migrator$ = new Subject(); private typeRegistry = new SavedObjectTypeRegistry(); private validations: PropertyValidators = {}; @@ -262,6 +264,7 @@ export class SavedObjectsService http: setupDeps.http, logger: this.logger, config: this.config, + migratorPromise: this.migrator$.pipe(first()).toPromise(), importableExportableTypes, }); @@ -302,6 +305,8 @@ export class SavedObjectsService const adminClient = this.setupDeps!.elasticsearch.adminClient; const migrator = this.createMigrator(kibanaConfig, this.config.migration, migrationsRetryDelay); + this.migrator$.next(migrator); + /** * Note: We want to ensure that migrations have completed before * continuing with further Core start steps that might use SavedObjects diff --git a/src/es_archiver/actions/empty_kibana_index.ts b/src/es_archiver/actions/empty_kibana_index.ts index 5f96fbc5f996c..d61d544deadc4 100644 --- a/src/es_archiver/actions/empty_kibana_index.ts +++ b/src/es_archiver/actions/empty_kibana_index.ts @@ -32,9 +32,8 @@ export async function emptyKibanaIndexAction({ kbnClient: KbnClient; }) { const stats = createStats('emptyKibanaIndex', log); - const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); await deleteKibanaIndices({ client, stats, log }); - await migrateKibanaIndex({ client, log, kibanaPluginIds }); + await migrateKibanaIndex({ client, kbnClient }); return stats; } diff --git a/src/es_archiver/actions/load.ts b/src/es_archiver/actions/load.ts index 404fd0daea91d..ae7799205b299 100644 --- a/src/es_archiver/actions/load.ts +++ b/src/es_archiver/actions/load.ts @@ -106,7 +106,7 @@ export async function loadAction({ // If we affected the Kibana index, we need to ensure it's migrated... if (Object.keys(result).some(k => k.startsWith('.kibana'))) { - await migrateKibanaIndex({ client, log, kibanaPluginIds }); + await migrateKibanaIndex({ client, kbnClient }); if (kibanaPluginIds.includes('spaces')) { await createDefaultSpace({ client, index: '.kibana' }); diff --git a/src/es_archiver/lib/indices/kibana_index.ts b/src/es_archiver/lib/indices/kibana_index.ts index 56be86a23eda0..1867f24d6f9ed 100644 --- a/src/es_archiver/lib/indices/kibana_index.ts +++ b/src/es_archiver/lib/indices/kibana_index.ts @@ -17,42 +17,10 @@ * under the License. */ -import { get } from 'lodash'; -import fs from 'fs'; -import Path from 'path'; -import { promisify } from 'util'; -import { toArray } from 'rxjs/operators'; import { Client, CreateDocumentParams } from 'elasticsearch'; -import { ToolingLog } from '@kbn/dev-utils'; - +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; -import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; -import { LegacyConfig } from '../../../core/server'; -import { convertLegacyTypes } from '../../../core/server/saved_objects/utils'; -import { SavedObjectTypeRegistry } from '../../../core/server/saved_objects'; -// @ts-ignore -import { collectUiExports } from '../../../legacy/ui/ui_exports'; -// @ts-ignore -import { findPluginSpecs } from '../../../legacy/plugin_discovery'; - -/** - * Load the uiExports for a Kibana instance, only load uiExports from xpack if - * it is enabled in the Kibana server. - */ -const getUiExports = async (kibanaPluginIds: string[]) => { - const xpackEnabled = kibanaPluginIds.includes('xpack_main'); - - const { spec$ } = await findPluginSpecs({ - plugins: { - scanDirs: [Path.resolve(__dirname, '../../../legacy/core_plugins')], - paths: xpackEnabled ? [Path.resolve(__dirname, '../../../../x-pack')] : [], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - return collectUiExports(specs); -}; /** * Deletes all indices that start with `.kibana` @@ -93,61 +61,21 @@ export async function deleteKibanaIndices({ */ export async function migrateKibanaIndex({ client, - log, - kibanaPluginIds, + kbnClient, }: { client: Client; - log: ToolingLog; - kibanaPluginIds: string[]; + kbnClient: KbnClient; }) { - const uiExports = await getUiExports(kibanaPluginIds); - const kibanaVersion = await loadKibanaVersion(); - - const configKeys: Record = { - 'xpack.task_manager.index': '.kibana_task_manager', - }; - const config = { get: (path: string) => configKeys[path] }; - - const savedObjectTypes = convertLegacyTypes(uiExports, config as LegacyConfig); - const typeRegistry = new SavedObjectTypeRegistry(); - savedObjectTypes.forEach(type => typeRegistry.registerType(type)); - - const logger = { - trace: log.verbose.bind(log), - debug: log.debug.bind(log), - info: log.info.bind(log), - warn: log.warning.bind(log), - error: log.error.bind(log), - fatal: log.error.bind(log), - log: (entry: any) => log.info(entry.message), - get: () => logger, - }; - - const migratorOptions = { - savedObjectsConfig: { - scrollDuration: '5m', - batchSize: 100, - pollInterval: 100, - skip: false, + // we allow dynamic mappings on the index, as some interceptors are accessing documents before + // the migration is actually performed. The migrator will put the value back to `strict` after migration. + await client.indices.putMapping({ + index: '.kibana', + body: { + dynamic: true, }, - kibanaConfig: { - index: '.kibana', - } as any, - logger, - kibanaVersion, - typeRegistry, - savedObjectValidations: uiExports.savedObjectValidations, - callCluster: (path: string, ...args: any[]) => - (get(client, path) as Function).call(client, ...args), - }; - - return await new KibanaMigrator(migratorOptions).runMigrations(); -} + } as any); -async function loadKibanaVersion() { - const readFile = promisify(fs.readFile); - const packageJson = await readFile(Path.join(__dirname, '../../../../package.json')); - return JSON.parse(packageJson.toString('utf-8')).version; + return await kbnClient.savedObjects.migrate(); } /** diff --git a/src/legacy/core_plugins/data/public/actions/filters/brush_event.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js deleted file mode 100644 index 67711bd4599a2..0000000000000 --- a/src/legacy/core_plugins/data/public/actions/filters/brush_event.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 _ from 'lodash'; -import moment from 'moment'; -import { esFilters } from '../../../../../../plugins/data/public'; -import { deserializeAggConfig } from '../../search/expressions/utils'; - -export async function onBrushEvent(event, getIndexPatterns) { - const isNumber = event.data.ordered; - const isDate = isNumber && event.data.ordered.date; - - const xRaw = _.get(event.data, 'series[0].values[0].xRaw'); - if (!xRaw) return []; - const column = xRaw.table.columns[xRaw.column]; - if (!column) return []; - if (!column.meta) return []; - const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); - const aggConfig = deserializeAggConfig({ - ...column.meta, - indexPattern, - }); - const field = aggConfig.params.field; - if (!field) return []; - - if (event.range.length <= 1) return []; - - const min = event.range[0]; - const max = event.range[event.range.length - 1]; - if (min === max) return []; - - let range; - - if (isDate) { - range = { - gte: moment(min).toISOString(), - lt: moment(max).toISOString(), - format: 'strict_date_optional_time', - }; - } else { - range = { - gte: min, - lt: max, - }; - } - - const newFilter = esFilters.buildRangeFilter( - field, - range, - indexPattern, - event.data.xAxisFormatter - ); - - return [newFilter]; -} diff --git a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js deleted file mode 100644 index 743f6caee4edd..0000000000000 --- a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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 _ from 'lodash'; -import moment from 'moment'; -import expect from '@kbn/expect'; - -jest.mock('../../search/aggs', () => ({ - AggConfigs: function AggConfigs() { - return { - createAggConfig: ({ params }) => ({ - params, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }), - }; - }, -})); - -import { onBrushEvent } from './brush_event'; - -describe('brushEvent', () => { - const DAY_IN_MS = 24 * 60 * 60 * 1000; - const JAN_01_2014 = 1388559600000; - - const aggConfigs = [ - { - params: {}, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }, - ]; - - const baseEvent = { - data: { - fieldFormatter: _.constant({}), - series: [ - { - values: [ - { - xRaw: { - column: 0, - table: { - columns: [ - { - id: '1', - meta: { - type: 'histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: aggConfigs[0].params, - }, - }, - ], - }, - }, - }, - ], - }, - ], - }, - }; - - beforeEach(() => { - baseEvent.data.indexPattern = { - id: 'logstash-*', - timeFieldName: 'time', - }; - }); - - test('should be a function', () => { - expect(onBrushEvent).to.be.a(Function); - }); - - test('ignores event when data.xAxisField not provided', async () => { - const event = _.cloneDeep(baseEvent); - const filters = await onBrushEvent(event, () => ({ - get: () => baseEvent.data.indexPattern, - })); - expect(filters.length).to.equal(0); - }); - - describe('handles an event when the x-axis field is a date field', () => { - describe('date field is index pattern timefield', () => { - let dateEvent; - const dateField = { - name: 'time', - type: 'date', - }; - - beforeEach(() => { - aggConfigs[0].params.field = dateField; - dateEvent = _.cloneDeep(baseEvent); - dateEvent.data.ordered = { date: true }; - }); - - test('by ignoring the event when range spans zero time', async () => { - const event = _.cloneDeep(dateEvent); - event.range = [JAN_01_2014, JAN_01_2014]; - const filters = await onBrushEvent(event, () => ({ - get: () => dateEvent.data.indexPattern, - })); - expect(filters.length).to.equal(0); - }); - - test('by updating the timefilter', async () => { - const event = _.cloneDeep(dateEvent); - event.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filters = await onBrushEvent(event, () => ({ - get: async () => dateEvent.data.indexPattern, - })); - expect(filters[0].range.time.gte).to.be(new Date(JAN_01_2014).toISOString()); - // Set to a baseline timezone for comparison. - expect(filters[0].range.time.lt).to.be(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); - }); - }); - - describe('date field is not index pattern timefield', () => { - let dateEvent; - const dateField = { - name: 'anotherTimeField', - type: 'date', - }; - - beforeEach(() => { - aggConfigs[0].params.field = dateField; - dateEvent = _.cloneDeep(baseEvent); - dateEvent.data.ordered = { date: true }; - }); - - test('creates a new range filter', async () => { - const event = _.cloneDeep(dateEvent); - const rangeBegin = JAN_01_2014; - const rangeEnd = rangeBegin + DAY_IN_MS; - event.range = [rangeBegin, rangeEnd]; - const filters = await onBrushEvent(event, () => ({ - get: () => dateEvent.data.indexPattern, - })); - expect(filters.length).to.equal(1); - expect(filters[0].range.anotherTimeField.gte).to.equal(moment(rangeBegin).toISOString()); - expect(filters[0].range.anotherTimeField.lt).to.equal(moment(rangeEnd).toISOString()); - expect(filters[0].range.anotherTimeField).to.have.property('format'); - expect(filters[0].range.anotherTimeField.format).to.equal('strict_date_optional_time'); - }); - }); - }); - - describe('handles an event when the x-axis field is a number', () => { - let numberEvent; - const numberField = { - name: 'numberField', - type: 'number', - }; - - beforeEach(() => { - aggConfigs[0].params.field = numberField; - numberEvent = _.cloneDeep(baseEvent); - numberEvent.data.ordered = { date: false }; - }); - - test('by ignoring the event when range does not span at least 2 values', async () => { - const event = _.cloneDeep(numberEvent); - event.range = [1]; - const filters = await onBrushEvent(event, () => ({ - get: () => numberEvent.data.indexPattern, - })); - expect(filters.length).to.equal(0); - }); - - test('by creating a new filter', async () => { - const event = _.cloneDeep(numberEvent); - event.range = [1, 2, 3, 4]; - const filters = await onBrushEvent(event, () => ({ - get: () => numberEvent.data.indexPattern, - })); - expect(filters.length).to.equal(1); - expect(filters[0].range.numberField.gte).to.equal(1); - expect(filters[0].range.numberField.lt).to.equal(4); - expect(filters[0].range.numberField).not.to.have.property('format'); - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts new file mode 100644 index 0000000000000..0e18c7c707fa3 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.ts @@ -0,0 +1,208 @@ +/* + * 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 moment from 'moment'; + +jest.mock('../../search/aggs', () => ({ + AggConfigs: function AggConfigs() { + return { + createAggConfig: ({ params }: Record) => ({ + params, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }), + }; + }, +})); + +jest.mock('../../../../../../plugins/data/public/services', () => ({ + getIndexPatterns: () => { + return { + get: async () => { + return { + id: 'logstash-*', + timeFieldName: 'time', + }; + }, + }; + }, +})); + +import { onBrushEvent, BrushEvent } from './brush_event'; + +describe('brushEvent', () => { + const DAY_IN_MS = 24 * 60 * 60 * 1000; + const JAN_01_2014 = 1388559600000; + let baseEvent: BrushEvent; + + const aggConfigs = [ + { + params: { + field: {}, + }, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }, + ]; + + beforeEach(() => { + baseEvent = { + data: { + ordered: { + date: false, + }, + series: [ + { + values: [ + { + xRaw: { + column: 0, + table: { + columns: [ + { + id: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + range: [], + }; + }); + + test('should be a function', () => { + expect(typeof onBrushEvent).toBe('function'); + }); + + test('ignores event when data.xAxisField not provided', async () => { + const filter = await onBrushEvent(baseEvent); + expect(filter).toBeUndefined(); + }); + + describe('handles an event when the x-axis field is a date field', () => { + describe('date field is index pattern timefield', () => { + beforeEach(() => { + aggConfigs[0].params.field = { + name: 'time', + type: 'date', + }; + baseEvent.data.ordered = { date: true }; + }); + + afterAll(() => { + baseEvent.range = []; + baseEvent.data.ordered = { date: false }; + }); + + test('by ignoring the event when range spans zero time', async () => { + baseEvent.range = [JAN_01_2014, JAN_01_2014]; + const filter = await onBrushEvent(baseEvent); + expect(filter).toBeUndefined(); + }); + + test('by updating the timefilter', async () => { + baseEvent.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; + const filter = await onBrushEvent(baseEvent); + expect(filter).toBeDefined(); + + if (filter) { + expect(filter.range.time.gte).toBe(new Date(JAN_01_2014).toISOString()); + // Set to a baseline timezone for comparison. + expect(filter.range.time.lt).toBe(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); + } + }); + }); + + describe('date field is not index pattern timefield', () => { + beforeEach(() => { + aggConfigs[0].params.field = { + name: 'anotherTimeField', + type: 'date', + }; + baseEvent.data.ordered = { date: true }; + }); + + afterAll(() => { + baseEvent.range = []; + baseEvent.data.ordered = { date: false }; + }); + + test('creates a new range filter', async () => { + const rangeBegin = JAN_01_2014; + const rangeEnd = rangeBegin + DAY_IN_MS; + baseEvent.range = [rangeBegin, rangeEnd]; + const filter = await onBrushEvent(baseEvent); + + expect(filter).toBeDefined(); + + if (filter) { + expect(filter.range.anotherTimeField.gte).toBe(moment(rangeBegin).toISOString()); + expect(filter.range.anotherTimeField.lt).toBe(moment(rangeEnd).toISOString()); + expect(filter.range.anotherTimeField).toHaveProperty( + 'format', + 'strict_date_optional_time' + ); + } + }); + }); + }); + + describe('handles an event when the x-axis field is a number', () => { + beforeAll(() => { + aggConfigs[0].params.field = { + name: 'numberField', + type: 'number', + }; + }); + + afterAll(() => { + baseEvent.range = []; + }); + + test('by ignoring the event when range does not span at least 2 values', async () => { + baseEvent.range = [1]; + const filter = await onBrushEvent(baseEvent); + expect(filter).toBeUndefined(); + }); + + test('by creating a new filter', async () => { + baseEvent.range = [1, 2, 3, 4]; + const filter = await onBrushEvent(baseEvent); + + expect(filter).toBeDefined(); + + if (filter) { + expect(filter.range.numberField.gte).toBe(1); + expect(filter.range.numberField.lt).toBe(4); + expect(filter.range.numberField).not.toHaveProperty('format'); + } + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/actions/filters/brush_event.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.ts new file mode 100644 index 0000000000000..00990d21ccf37 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.ts @@ -0,0 +1,80 @@ +/* + * 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 { get, last } from 'lodash'; +import moment from 'moment'; +import { esFilters, IFieldType, RangeFilterParams } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +// should be removed after moving into new platform plugins data folder +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; + +export interface BrushEvent { + data: { + ordered: { + date: boolean; + }; + series: Array>; + }; + range: number[]; +} + +export async function onBrushEvent(event: BrushEvent) { + const isDate = get(event.data, 'ordered.date'); + const xRaw: Record = get(event.data, 'series[0].values[0].xRaw'); + + if (!xRaw) { + return; + } + + const column: Record = xRaw.table.columns[xRaw.column]; + + if (!column || !column.meta) { + return; + } + + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); + const field: IFieldType = aggConfig.params.field; + + if (!field || event.range.length <= 1) { + return; + } + + const min = event.range[0]; + const max = last(event.range); + + if (min === max) { + return; + } + + const range: RangeFilterParams = { + gte: isDate ? moment(min).toISOString() : min, + lt: isDate ? moment(max).toISOString() : max, + }; + + if (isDate) { + range.format = 'strict_date_optional_time'; + } + + return esFilters.buildRangeFilter(field, range, indexPattern); +} diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts index 8d0b74be50535..7f1c5d78ab800 100644 --- a/src/legacy/core_plugins/data/public/actions/select_range_action.ts +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -23,16 +23,8 @@ import { createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; -// @ts-ignore import { onBrushEvent } from './filters/brush_event'; -import { - Filter, - FilterManager, - TimefilterContract, - esFilters, -} from '../../../../../plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getIndexPatterns } from '../../../../../plugins/data/public/services'; +import { FilterManager, TimefilterContract, esFilters } from '../../../../../plugins/data/public'; export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; @@ -43,8 +35,7 @@ interface ActionContext { async function isCompatible(context: ActionContext) { try { - const filters: Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; - return filters.length > 0; + return Boolean(await onBrushEvent(context.data)); } catch { return false; } @@ -68,9 +59,13 @@ export function selectRangeAction( throw new IncompatibleActionError(); } - const filters: Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; + const filter = await onBrushEvent(data); + + if (!filter) { + return; + } - const selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); + const selectedFilters = esFilters.mapAndFlattenFilters([filter]); if (timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index ce46f534141f4..8cde5d0a1fc11 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -54,6 +54,7 @@ export * from '../common'; export { FilterStateManager } from './filter/filter_manager'; export { // agg_types TODO need to group these under a namespace or prefix + AggConfigs, AggParamType, AggTypeFilters, // TODO convert to interface aggTypeFilters, @@ -66,6 +67,7 @@ export { convertIPRangeToString, intervalOptions, // only used in Discover isDateHistogramBucketAggConfig, + setBounds, isStringType, isType, isValidInterval, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index bf5049cd976a3..1ac54ad5dabee 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -556,8 +556,9 @@ function discoverController( $scope.opts = { // number of records to fetch, then paginate through sampleSize: config.get('discover:sampleSize'), - timefield: - indexPatternsUtils.isDefault($scope.indexPattern) && $scope.indexPattern.timeFieldName, + timefield: indexPatternsUtils.isDefault($scope.indexPattern) + ? $scope.indexPattern.timeFieldName + : undefined, savedSearch: savedSearch, indexPatternList: $route.current.locals.savedObjects.ip.list, }; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index d77fc780f4cc2..384c6bd80ec33 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -53,7 +53,7 @@ import './management'; import './dev_tools'; import 'ui/agg_response'; import 'ui/agg_types'; -import { showAppRedirectNotification } from 'ui/notify'; +import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; import 'leaflet'; import { localApplicationService } from './local_application_service'; @@ -68,4 +68,6 @@ routes.otherwise({ redirectTo: `/${config.defaultAppId || 'discover'}`, }); -uiModules.get('kibana').run(showAppRedirectNotification); +uiModules + .get('kibana') + .run($location => showAppRedirectNotification($location, npSetup.core.notifications.toasts)); diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 096877d5824c4..cfd12b3283459 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -36,6 +36,7 @@ import { VisualizationsStart } from '../../../visualizations/public'; import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; +import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizeKibanaServices { pluginInitializerContext: PluginInitializerContext; @@ -60,6 +61,7 @@ export interface VisualizeKibanaServices { usageCollection?: UsageCollectionSetup; I18nContext: I18nStart['Context']; setActiveUrl: (newUrl: string) => void; + DefaultVisualizationEditor: typeof DefaultEditorController; } let services: VisualizeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 8b1bb0fda8c84..d9565938c838d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -43,8 +43,7 @@ export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { wrapInI18nContext } from 'ui/i18n'; export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; -export { VisSavedObject } from '../../../visualizations/public/embeddable/visualize_embeddable'; -export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; +export { VisSavedObject, VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/'; export { configureAppAngularModule, ensureDefaultIndexPattern, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index c40a10115ae4e..65c25b8cf705d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -30,7 +30,7 @@ export function initVisEditorDirective(app, deps) { appState: '=', }, link: function($scope, element) { - const Editor = $scope.savedObj.vis.type.editor; + const Editor = $scope.savedObj.vis.type.editor || deps.DefaultVisualizationEditor; const editor = new Editor(element[0], $scope.savedObj); $scope.renderFunction = () => { diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index dc56a2e02b9ab..b9e4487ae84fb 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -51,6 +51,7 @@ import { HomePublicPluginSetup, } from '../../../../../plugins/home/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; +import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -144,7 +145,7 @@ export class VisualizePlugin implements Plugin { localStorage: new Storage(localStorage), navigation, savedObjectsClient, - savedVisualizations: visualizations.getSavedVisualizationsLoader(), + savedVisualizations: visualizations.savedVisualizationsLoader, savedQueryService: dataStart.query.savedQueries, share, toastNotifications: coreStart.notifications.toasts, @@ -155,6 +156,7 @@ export class VisualizePlugin implements Plugin { usageCollection, I18nContext: coreStart.i18n.Context, setActiveUrl, + DefaultVisualizationEditor: DefaultEditorController, }; setServices(deps); diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index 0394dea343adf..715ca56e290a2 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -20,6 +20,7 @@ import { encryptTelemetry } from './collectors'; import { CallCluster } from '../../elasticsearch'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; +import { ESLicense } from './telemetry_collection/get_local_license'; export type EncryptedStatsGetterConfig = { unencrypted: false } & { server: any; @@ -45,22 +46,38 @@ export interface StatsCollectionConfig { end: string | number; } +export interface BasicStatsPayload { + timestamp: string; + cluster_uuid: string; + cluster_name: string; + version: string; + cluster_stats: object; + collection?: string; + stack_stats: object; +} + export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig; export type ClusterDetailsGetter = (config: StatsCollectionConfig) => Promise; -export type StatsGetter = ( +export type StatsGetter = ( + clustersDetails: ClusterDetails[], + config: StatsCollectionConfig +) => Promise; +export type LicenseGetter = ( clustersDetails: ClusterDetails[], config: StatsCollectionConfig -) => Promise; +) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>; -interface CollectionConfig { +interface CollectionConfig { title: string; priority: number; esCluster: string; - statsGetter: StatsGetter; + statsGetter: StatsGetter; clusterDetailsGetter: ClusterDetailsGetter; + licenseGetter: LicenseGetter; } interface Collection { statsGetter: StatsGetter; + licenseGetter: LicenseGetter; clusterDetailsGetter: ClusterDetailsGetter; esCluster: string; title: string; @@ -70,8 +87,15 @@ export class TelemetryCollectionManager { private usageGetterMethodPriority = -1; private collections: Collection[] = []; - public setCollection = (collectionConfig: CollectionConfig) => { - const { title, priority, esCluster, statsGetter, clusterDetailsGetter } = collectionConfig; + public setCollection = (collectionConfig: CollectionConfig) => { + const { + title, + priority, + esCluster, + statsGetter, + clusterDetailsGetter, + licenseGetter, + } = collectionConfig; if (typeof priority !== 'number') { throw new Error('priority must be set.'); @@ -88,10 +112,14 @@ export class TelemetryCollectionManager { throw Error('esCluster name must be set for the getCluster method.'); } if (!clusterDetailsGetter) { - throw Error('Cluser UUIds method is not set.'); + throw Error('Cluster UUIds method is not set.'); + } + if (!licenseGetter) { + throw Error('License getter method not set.'); } this.collections.unshift({ + licenseGetter, statsGetter, clusterDetailsGetter, esCluster, @@ -141,7 +169,19 @@ export class TelemetryCollectionManager { return; } - return await collection.statsGetter(clustersDetails, statsCollectionConfig); + const [stats, licenses] = await Promise.all([ + collection.statsGetter(clustersDetails, statsCollectionConfig), + collection.licenseGetter(clustersDetails, statsCollectionConfig), + ]); + + return stats.map(stat => { + const license = licenses[stat.cluster_uuid]; + return { + ...(license ? { license } : {}), + ...stat, + collectionSource: collection.title, + }; + }); }; public getOptInStats = async (optInStatus: boolean, config: StatsGetterConfig) => { diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts new file mode 100644 index 0000000000000..67812457ed4ec --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.ts @@ -0,0 +1,48 @@ +/* + * 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 { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +// This can be removed when the ES client improves the types +export interface ESClusterInfo { + cluster_uuid: string; + cluster_name: string; + version: { + number: string; + build_flavor: string; + build_type: string; + build_hash: string; + build_date: string; + build_snapshot?: boolean; + lucene_version: string; + minimum_wire_compatibility_version: string; + minimum_index_compatibility_version: string; + }; +} + +/** + * Get the cluster info from the connected cluster. + * + * This is the equivalent to GET / + * + * @param {function} callCluster The callWithInternalUser handler (exposed for testing) + */ +export function getClusterInfo(callCluster: CallCluster) { + return callCluster('info'); +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts new file mode 100644 index 0000000000000..589392ffb6095 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license.ts @@ -0,0 +1,90 @@ +/* + * 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 { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LicenseGetter } from '../collection_manager'; + +// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html +export interface ESLicense { + status: string; + uid: string; + type: string; + issue_date: string; + issue_date_in_millis: number; + expiry_date: string; + expirty_date_in_millis: number; + max_nodes: number; + issued_to: string; + issuer: string; + start_date_in_millis: number; +} +let cachedLicense: ESLicense | undefined; + +function fetchLicense(callCluster: CallCluster, local: boolean) { + return callCluster<{ license: ESLicense }>('transport.request', { + method: 'GET', + path: '/_license', + query: { + local, + // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. + accept_enterprise: 'true', + }, + }); +} + +/** + * Get the cluster's license from the connected node. + * + * This is the equivalent of GET /_license?local=true . + * + * Like any X-Pack related API, X-Pack must installed for this to work. + */ +async function getLicenseFromLocalOrMaster(callCluster: CallCluster) { + // Fetching the local license is cheaper than getting it from the master and good enough + const { license } = await fetchLicense(callCluster, true).catch(async err => { + if (cachedLicense) { + try { + // Fallback to the master node's license info + const response = await fetchLicense(callCluster, false); + return response; + } catch (masterError) { + if (masterError.statusCode === 404) { + // If the master node does not have a license, we can assume there is no license + cachedLicense = undefined; + } else { + // Any other errors from the master node, throw and do not send any telemetry + throw err; + } + } + } + return { license: void 0 }; + }); + + if (license) { + cachedLicense = license; + } + return license; +} + +export const getLocalLicense: LicenseGetter = async (clustersDetails, { callCluster }) => { + const license = await getLicenseFromLocalOrMaster(callCluster); + + // It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case. + return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {}); +}; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 8adb6d237bee8..d99710deb1cbc 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -17,11 +17,8 @@ * under the License. */ -import { get, omit } from 'lodash'; -// @ts-ignore -import { getClusterInfo } from './get_cluster_info'; +import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; -// @ts-ignore import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; import { StatsGetter } from '../collection_manager'; @@ -33,20 +30,19 @@ import { StatsGetter } from '../collection_manager'; * @param {Object} clusterInfo Cluster info (GET /) * @param {Object} clusterStats Cluster stats (GET /_cluster/stats) * @param {Object} kibana The Kibana Usage stats - * @return {Object} A combined object containing the different responses. */ export function handleLocalStats( server: any, - clusterInfo: any, - clusterStats: any, + { cluster_name, cluster_uuid, version }: ESClusterInfo, + { _nodes, cluster_name: clusterName, ...clusterStats }: any, kibana: KibanaUsageStats ) { return { timestamp: new Date().toISOString(), - cluster_uuid: get(clusterInfo, 'cluster_uuid'), - cluster_name: get(clusterInfo, 'cluster_name'), - version: get(clusterInfo, 'version.number'), - cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'), + cluster_uuid, + cluster_name, + version: version.number, + cluster_stats: clusterStats, collection: 'local', stack_stats: { kibana: handleKibanaStats(server, kibana), @@ -54,14 +50,12 @@ export function handleLocalStats( }; } +export type TelemetryLocalStats = ReturnType; + /** * Get statistics for all products joined by Elasticsearch cluster. - * - * @param {Object} server The Kibana server instance used to call ES as the internal user - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ -export const getLocalStats: StatsGetter = async (clustersDetails, config) => { +export const getLocalStats: StatsGetter = async (clustersDetails, config) => { const { server, callCluster, usageCollection } = config; return await Promise.all( diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts index 7f228dbc5e6f6..9ac94216c21bc 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts @@ -17,7 +17,6 @@ * under the License. */ -// @ts-ignore export { getLocalStats } from './get_local_stats'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts index faf8e9de79194..6580b47dba08e 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -39,6 +39,7 @@ import { telemetryCollectionManager } from '../collection_manager'; import { getLocalStats } from './get_local_stats'; import { getClusterUuids } from './get_cluster_stats'; +import { getLocalLicense } from './get_local_license'; export function registerCollection() { telemetryCollectionManager.setCollection({ @@ -47,5 +48,6 @@ export function registerCollection() { priority: 0, statsGetter: getLocalStats, clusterDetailsGetter: getClusterUuids, + licenseGetter: getLocalLicense, }); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx index 32ea71c0bc005..7eee54006f684 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx @@ -20,8 +20,10 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EditorRenderProps } from '../../kibana/public/visualize/np_ready/types'; -import { VisualizeEmbeddable } from '../../visualizations/public/embeddable'; -import { VisualizeEmbeddableFactory } from '../../visualizations/public/embeddable/visualize_embeddable_factory'; +import { + VisualizeEmbeddableContract as VisualizeEmbeddable, + VisualizeEmbeddableFactoryContract as VisualizeEmbeddableFactory, +} from '../../visualizations/public/'; import { PanelsContainer, Panel } from '../../../../plugins/kibana_react/public'; import './vis_type_agg_filter'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx index d3090d277aef9..db910604eddd1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; -import { VisSavedObject } from 'src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable'; +import { VisSavedObject } from 'src/legacy/core_plugins/visualizations/public/'; import { Storage } from '../../../../plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../plugins/kibana_react/public'; import { DefaultEditor } from './default_editor'; diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index 5729618b6ae07..8cc0ca2456867 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -47,14 +47,14 @@ function TableOptions({ .filter(col => get(col.aggConfig.type.getFormat(col.aggConfig), 'type.id') === 'number') .map(({ name }) => ({ value: name, text: name })), ], - [aggs, stateParams.percentageCol, stateParams.dimensions] + [aggs] ); const isPerPageValid = stateParams.perPage === '' || stateParams.perPage > 0; useEffect(() => { setValidity(isPerPageValid); - }, [isPerPageValid]); + }, [isPerPageValid, setValidity]); useEffect(() => { if ( @@ -64,7 +64,7 @@ function TableOptions({ ) { setValue('percentageCol', percentageColumns[0].value); } - }, [percentageColumns, stateParams.percentageCol]); + }, [percentageColumns, stateParams.percentageCol, setValidity, setValue]); return ( diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts b/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts deleted file mode 100644 index f37bc858efab0..0000000000000 --- a/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { toastNotifications } from 'ui/notify'; - -import { IAggConfig } from 'ui/agg_types'; -import { timefilter } from 'ui/timefilter'; -import { Vis } from '../np_ready/public'; -import { Filter, Query, SearchSource, ISearchSource } from '../../../../../plugins/data/public'; - -interface QueryGeohashBoundsParams { - filters?: Filter[]; - query?: Query; - searchSource?: ISearchSource; -} - -/** - * Coordinate map visualization needs to be able to query for the latest geohash - * bounds when a user clicks the "fit to data" map icon, which requires knowing - * about global filters & queries. This logic has been extracted here so we can - * keep `searchSource` out of the vis, but ultimately we need to design a - * long-term solution for situations like this. - * - * TODO: Remove this as a part of elastic/kibana#30593 - */ -export async function queryGeohashBounds(vis: Vis, params: QueryGeohashBoundsParams) { - const agg = vis.getAggConfig().aggs.find((a: IAggConfig) => { - return get(a, 'type.dslName') === 'geohash_grid'; - }); - - if (agg) { - const searchSource = params.searchSource - ? params.searchSource.createChild() - : new SearchSource(); - searchSource.setField('size', 0); - searchSource.setField('aggs', () => { - const geoBoundsAgg = vis.getAggConfig().createAggConfig( - { - type: 'geo_bounds', - enabled: true, - params: { - field: agg.getField(), - }, - schema: 'metric', - }, - { - addToAggConfigs: false, - } - ); - return { - '1': geoBoundsAgg.toDsl(), - }; - }); - - const { filters, query } = params; - if (filters) { - searchSource.setField('filter', () => { - const activeFilters = [...filters]; - const indexPattern = agg.getIndexPattern(); - const useTimeFilter = !!indexPattern.timeFieldName; - if (useTimeFilter) { - const filter = timefilter.createFilter(indexPattern); - if (filter) activeFilters.push((filter as any) as Filter); - } - return activeFilters; - }); - } - if (query) { - searchSource.setField('query', query); - } - - try { - const esResp = await searchSource.fetch(); - return get(esResp, 'aggregations.1.bounds'); - } catch (error) { - toastNotifications.addDanger({ - title: i18n.translate('visualizations.queryGeohashBounds.unableToGetBoundErrorTitle', { - defaultMessage: 'Unable to get bounds', - }), - text: `${error.message}`, - }); - return; - } - } -} diff --git a/src/legacy/core_plugins/visualizations/public/index.scss b/src/legacy/core_plugins/visualizations/public/index.scss index 748945eabd331..238f58fbfa295 100644 --- a/src/legacy/core_plugins/visualizations/public/index.scss +++ b/src/legacy/core_plugins/visualizations/public/index.scss @@ -1,4 +1,2 @@ @import 'src/legacy/ui/public/styles/styling_constants'; - -@import './embeddable/index'; @import './np_ready/public/index'; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index 5cff588d951b0..223c130df3505 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -24,9 +24,5 @@ export { IAggConfigs, isDateHistogramBucketAggConfig, setBounds, -} from '../../../ui/public/agg_types'; -export { createFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; -export { I18nContext } from '../../../ui/public/i18n'; -import chrome from '../../../ui/public/chrome'; -export { chrome as legacyChrome }; -import '../../../ui/public/directives/bind'; +} from '../../data/public'; +export { createSavedSearchesLoader } from '../../kibana/public/discover/saved_searches/'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json b/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json index 888edde44a261..d4f9bd327d6ac 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json +++ b/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json @@ -3,8 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": [ - "data", - "search" - ] + "requiredPlugins": ["data", "search", "expressions", "uiActions"] } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss index d87b6b004a511..eada763b63c4d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss @@ -1 +1,2 @@ @import 'wizard/index'; +@import 'embeddable/index'; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/_embeddables.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_embeddables.scss similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/_embeddables.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_embeddables.scss diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/_index.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_index.scss similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/_index.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_index.scss diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_visualize_lab_disabled.scss similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/_visualize_lab_disabled.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/_visualize_lab_disabled.scss diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/constants.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/constants.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/constants.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/constants.ts diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_embeddable.tsx similarity index 94% rename from src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_embeddable.tsx index f9dfd5d2b98f4..fbb2eba3afe79 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_embeddable.tsx @@ -19,7 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Embeddable, EmbeddableOutput } from '../../../../../plugins/embeddable/public'; +import { Embeddable, EmbeddableOutput } from '../../../../../../../plugins/embeddable/public'; import { DisabledLabVisualization } from './disabled_lab_visualization'; import { VisualizeInput } from './visualize_embeddable'; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_visualization.tsx similarity index 100% rename from src/legacy/core_plugins/visualizations/public/embeddable/disabled_lab_visualization.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/disabled_lab_visualization.tsx diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/get_index_pattern.ts similarity index 90% rename from src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/get_index_pattern.ts index cfb2960cfbb7c..51d839275fd27 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/get_index_pattern.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/get_index_pattern.ts @@ -17,13 +17,13 @@ * under the License. */ -import { VisSavedObject } from './visualize_embeddable'; +import { VisSavedObject } from '../types'; import { indexPatterns, IIndexPattern, IndexPatternAttributes, -} from '../../../../../plugins/data/public'; -import { getUISettings, getSavedObjects } from '../np_ready/public/services'; +} from '../../../../../../../plugins/data/public'; +import { getUISettings, getSavedObjects } from '../services'; export async function getIndexPattern( savedVis: VisSavedObject diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/index.ts similarity index 92% rename from src/legacy/core_plugins/visualizations/public/embeddable/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/index.ts index d7c0205891ec5..a1cd31eebef20 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/index.ts @@ -18,4 +18,5 @@ */ export { DisabledLabEmbeddable } from './disabled_lab_embeddable'; export { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable'; +export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; export { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts similarity index 90% rename from src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts index 32bbae13b79b8..2537caa01cd46 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts @@ -18,13 +18,8 @@ */ import _, { get } from 'lodash'; -import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; -import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; -import { npStart } from 'ui/new_platform'; -import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; -import { EmbeddableVisTriggerContext } from 'src/plugins/embeddable/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -32,9 +27,8 @@ import { Query, esFilters, Filter, - ISearchSource, TimefilterContract, -} from '../../../../../plugins/data/public'; +} from '../../../../../../../plugins/data/public'; import { EmbeddableInput, EmbeddableOutput, @@ -42,27 +36,21 @@ import { Container, selectRangeTrigger, valueClickTrigger, -} from '../../../../../plugins/embeddable/public'; -import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; -import { SavedObject } from '../../../../../plugins/saved_objects/public'; -import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; -import { Vis } from '../np_ready/public'; + EmbeddableVisTriggerContext, +} from '../../../../../../../plugins/embeddable/public'; +import { dispatchRenderComplete } from '../../../../../../../plugins/kibana_utils/public'; +import { + IExpressionLoaderParams, + ExpressionsStart, +} from '../../../../../../../plugins/expressions/public'; +import { buildPipeline } from '../legacy/build_pipeline'; +import { Vis } from '../vis'; +import { getExpressions, getUiActions } from '../services'; +import { PersistedState } from '../../../legacy_imports'; +import { VisSavedObject } from '../types'; const getKeys = (o: T): Array => Object.keys(o) as Array; -export interface VisSavedObject extends SavedObject { - vis: Vis; - description?: string; - searchSource: ISearchSource; - title: string; - uiStateJSON?: string; - destroy: () => void; - savedSearchRefName?: string; - savedSearchId?: string; - savedSearch?: SavedSearch; - visState: any; -} - export interface VisualizeEmbeddableConfiguration { savedVisualization: VisSavedObject; indexPatterns?: IIndexPattern[]; @@ -90,7 +78,7 @@ export interface VisualizeOutput extends EmbeddableOutput { visTypeName: string; } -type ExpressionLoader = InstanceType; +type ExpressionLoader = InstanceType; export class VisualizeEmbeddable extends Embeddable { private handler?: ExpressionLoader; @@ -281,7 +269,8 @@ export class VisualizeEmbeddable extends Embeddable { @@ -309,7 +298,9 @@ export class VisualizeEmbeddable extends Embeddable { public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - constructor( - private timefilter: TimefilterContract, - private getSavedVisualizationsLoader: () => SavedVisualizations - ) { + constructor() { super({ savedObjectMetaData: { name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), @@ -101,7 +98,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< input: Partial & { id: string }, parent?: Container ): Promise { - const savedVisualizations = this.getSavedVisualizationsLoader(); + const savedVisualizations = getSavedVisualizationsLoader(); try { const visId = savedObject.id as string; @@ -118,7 +115,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< const indexPattern = await getIndexPattern(savedObject); const indexPatterns = indexPattern ? [indexPattern] : []; return new VisualizeEmbeddable( - this.timefilter, + getTimeFilter(), { savedVisualization: savedObject, indexPatterns, @@ -141,7 +138,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< input: Partial & { id: string }, parent?: Container ): Promise { - const savedVisualizations = this.getSavedVisualizationsLoader(); + const savedVisualizations = getSavedVisualizationsLoader(); try { const savedObject = await savedVisualizations.get(savedObjectId); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts index f1c1677a60f26..4ac0931c5d865 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts @@ -19,8 +19,11 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { VisResponseValue } from 'src/plugins/visualizations/public'; -import { ExpressionFunctionDefinition, Render } from 'src/plugins/expressions/public'; +import { VisResponseValue } from '../../../../../../../plugins/visualizations/public'; +import { + ExpressionFunctionDefinition, + Render, +} from '../../../../../../../plugins/expressions/public'; import { PersistedState } from '../../../legacy_imports'; import { getTypes, getIndexPatterns, getFilterManager } from '../services'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 3c4a1c1449d47..34ffb698e5f8c 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -29,22 +29,28 @@ * either types, or static code. */ -import { PluginInitializerContext } from 'src/core/public'; +import { PublicContract } from '@kbn/utility-types'; +import { PluginInitializerContext } from '../../../../../../core/public'; import { VisualizationsPlugin, VisualizationsSetup, VisualizationsStart } from './plugin'; /** @public */ export { VisualizationsSetup, VisualizationsStart }; /** @public types */ -export { VisTypeAlias, VisType } from './types'; +export { VisTypeAlias, VisType } from './vis_types'; +export { VisSavedObject } from './types'; +export { Vis, VisParams, VisState } from './vis'; +import { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable'; +export type VisualizeEmbeddableFactoryContract = PublicContract; +export type VisualizeEmbeddableContract = PublicContract; export function plugin(initializerContext: PluginInitializerContext) { return new VisualizationsPlugin(initializerContext); } /** @public static code */ -export { Vis, VisParams, VisState } from './vis'; -export { TypesService } from './types/types_service'; +export { TypesService } from './vis_types/types_service'; +export { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from './embeddable'; export { Status } from './legacy/update_status'; export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/build_pipeline'; @@ -52,4 +58,4 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; -export { createSavedVisLoader } from '../../saved_visualizations/saved_visualizations'; +export { createSavedVisLoader } from './saved_visualizations/saved_visualizations'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts index 41b23b276e88d..57c686b6e9cb0 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts @@ -17,12 +17,12 @@ * under the License. */ -import { PluginInitializerContext } from 'src/core/public'; - /* eslint-disable @kbn/eslint/no-restricted-paths */ import { npSetup, npStart } from 'ui/new_platform'; /* eslint-enable @kbn/eslint/no-restricted-paths */ +import { PluginInitializerContext } from '../../../../../../core/public'; + import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js index b8aa33d0a5abe..9c1dfd9780255 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { BaseVisType } from '../../../types/base_vis_type'; +import { BaseVisType } from '../../../vis_types/base_vis_type'; describe('Base Vis Type', function() { beforeEach(ngMock.module('kibana')); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js index c85557ea1b0b0..2474a58870424 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { ReactVisType } from '../../../types/react_vis_type'; +import { ReactVisType } from '../../../vis_types/react_vis_type'; describe('React Vis Type', function() { const visConfig = { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts index f73dc3e19d0ef..1adf6fd23f5a5 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts @@ -31,11 +31,6 @@ import { IAggConfig } from '../../../legacy_imports'; import { searchSourceMock } from '../../../legacy_mocks'; jest.mock('ui/new_platform'); -jest.mock('ui/agg_types', () => ({ - setBounds: () => {}, - dateHistogramBucketAgg: () => {}, - isDateHistogramBucketAggConfig: () => true, -})); describe('visualize loader pipeline helpers: build pipeline', () => { describe('prepareJson', () => { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts index 025eef834ca86..155213b4103b0 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts @@ -18,17 +18,11 @@ */ import { cloneDeep, get } from 'lodash'; -// @ts-ignore import moment from 'moment'; -import { SerializedFieldFormat } from 'src/plugins/expressions/public'; -import { ISearchSource } from 'src/plugins/data/public'; -import { - IAggConfig, - setBounds, - isDateHistogramBucketAggConfig, - createFormat, -} from '../../../legacy_imports'; -import { Vis, VisParams } from '..'; +import { SerializedFieldFormat } from '../../../../../../../plugins/expressions/public'; +import { fieldFormats, ISearchSource } from '../../../../../../../plugins/data/public'; +import { IAggConfig, setBounds, isDateHistogramBucketAggConfig } from '../../../legacy_imports'; +import { Vis, VisParams } from '../types'; interface SchemaConfigParams { precision?: number; @@ -102,7 +96,7 @@ export const getSchemas = (vis: Vis, timeRange?: any): Schemas => { 'max_bucket', ].includes(agg.type.name); - const format = createFormat( + const format = fieldFormats.serialize( hasSubAgg ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) : agg diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index 9fb87cadb2983..b3dd22f62f81f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -23,7 +23,7 @@ jest.mock('ui/vis/vis_factory'); jest.mock('ui/registry/vis_types'); jest.mock('./types/vis_type_alias_registry'); -import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext } from '../../../../../../core/public'; import { VisualizationsSetup, VisualizationsStart } from './'; import { VisualizationsPlugin } from './plugin'; import { coreMock } from '../../../../../../core/public/mocks'; @@ -31,6 +31,7 @@ import { embeddablePluginMock } from '../../../../../../plugins/embeddable/publi import { expressionsPluginMock } from '../../../../../../plugins/expressions/public/mocks'; import { dataPluginMock } from '../../../../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../../../../plugins/usage_collection/public/mocks'; +import { uiActionsPluginMock } from '../../../../../../plugins/ui_actions/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ types: { @@ -47,7 +48,7 @@ const createStartContract = (): VisualizationsStart => ({ all: jest.fn(), getAliases: jest.fn(), }, - getSavedVisualizationsLoader: jest.fn(), + savedVisualizationsLoader: {} as any, showNewVisModal: jest.fn(), Vis: jest.fn(), }); @@ -64,6 +65,8 @@ const createInstance = async () => { const doStart = () => plugin.start(coreMock.createStart(), { data: dataPluginMock.createStartContract(), + expressions: expressionsPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), }); return { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 20bed59faad88..e1d87d414d398 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -17,8 +17,13 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { TypesService, TypesSetup, TypesStart } from './types'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, +} from '../../../../../../core/public'; +import { TypesService, TypesSetup, TypesStart } from './vis_types'; import { setUISettings, setTypes, @@ -29,10 +34,13 @@ import { setSavedObjects, setUsageCollector, setFilterManager, + setExpressions, + setUiActions, + setSavedVisualizationsLoader, + setTimeFilter, } from './services'; -import { VisualizeEmbeddableFactory } from '../../embeddable/visualize_embeddable_factory'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../embeddable'; -import { ExpressionsSetup } from '../../../../../../plugins/expressions/public'; +import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../../../plugins/expressions/public'; import { IEmbeddableSetup } from '../../../../../../plugins/embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; @@ -41,13 +49,11 @@ import { DataPublicPluginStart, } from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; -import { - createSavedVisLoader, - SavedObjectKibanaServicesWithVisualizations, -} from '../../saved_visualizations'; -import { SavedVisualizations } from '../../../../kibana/public/visualize/np_ready/types'; +import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations'; import { VisImpl, VisImplConstructor } from './vis_impl'; import { showNewVisModal } from './wizard'; +import { UiActionsStart } from '../../../../../../plugins/ui_actions/public'; + /** * Interface for this plugin's returned setup/start contracts. * @@ -59,9 +65,9 @@ export interface VisualizationsSetup { export interface VisualizationsStart { types: TypesStart; - getSavedVisualizationsLoader: () => SavedVisualizations; - showNewVisModal: typeof showNewVisModal; + savedVisualizationsLoader: SavedVisualizationsLoader; Vis: VisImplConstructor; + showNewVisModal: typeof showNewVisModal; } export interface VisualizationsSetupDeps { @@ -73,6 +79,8 @@ export interface VisualizationsSetupDeps { export interface VisualizationsStartDeps { data: DataPublicPluginStart; + expressions: ExpressionsStart; + uiActions: UiActionsStart; } /** @@ -92,8 +100,6 @@ export class VisualizationsPlugin VisualizationsStartDeps > { private readonly types: TypesService = new TypesService(); - private savedVisualizations?: SavedVisualizations; - private savedVisualizationDependencies?: SavedObjectKibanaServicesWithVisualizations; constructor(initializerContext: PluginInitializerContext) {} @@ -107,10 +113,7 @@ export class VisualizationsPlugin expressions.registerFunction(visualizationFunction); expressions.registerRenderer(visualizationRenderer); - const embeddableFactory = new VisualizeEmbeddableFactory( - data.query.timefilter.timefilter, - this.getSavedVisualizationsLoader - ); + const embeddableFactory = new VisualizeEmbeddableFactory(); embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory); return { @@ -118,7 +121,10 @@ export class VisualizationsPlugin }; } - public start(core: CoreStart, { data }: VisualizationsStartDeps): VisualizationsStart { + public start( + core: CoreStart, + { data, expressions, uiActions }: VisualizationsStartDeps + ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); setTypes(types); @@ -127,31 +133,27 @@ export class VisualizationsPlugin setSavedObjects(core.savedObjects); setIndexPatterns(data.indexPatterns); setFilterManager(data.query.filterManager); - - this.savedVisualizationDependencies = { + setExpressions(expressions); + setUiActions(uiActions); + setTimeFilter(data.query.timefilter.timefilter); + const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, chrome: core.chrome, overlays: core.overlays, visualizationTypes: types, - }; + }); + setSavedVisualizationsLoader(savedVisualizationsLoader); return { types, - getSavedVisualizationsLoader: () => this.getSavedVisualizationsLoader(), showNewVisModal, Vis: VisImpl, + savedVisualizationsLoader, }; } public stop() { this.types.stop(); } - - private getSavedVisualizationsLoader = () => { - if (!this.savedVisualizations) { - this.savedVisualizations = createSavedVisLoader(this.savedVisualizationDependencies!); - } - return this.savedVisualizations; - }; } diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts similarity index 90% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/_saved_vis.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts index f4548da375216..f3539b3564c56 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts @@ -28,15 +28,13 @@ import { createSavedObjectClass, SavedObject, SavedObjectKibanaServices, -} from '../../../../../plugins/saved_objects/public'; -import { updateOldState } from '../index'; +} from '../../../../../../../plugins/saved_objects/public'; +import { updateOldState } from '../../../index'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { IIndexPattern } from '../../../../../plugins/data/public'; -import { VisSavedObject } from '../embeddable/visualize_embeddable'; - -import { createSavedSearchesLoader } from '../../../kibana/public/discover'; -import { VisualizeConstants } from '../../../kibana/public/visualize'; -import { VisImpl } from '../np_ready/public/vis_impl'; +import { IIndexPattern } from '../../../../../../../plugins/data/public'; +import { VisSavedObject } from '../types'; +import { VisImpl } from '../vis_impl'; +import { createSavedSearchesLoader } from '../../../legacy_imports'; async function _afterEsResp(savedVis: VisSavedObject, services: any) { await _getLinkedSavedSearch(savedVis, services); @@ -138,7 +136,7 @@ export function createSavedVisClass(services: SavedObjectKibanaServices) { }); this.showInRecentlyAccessed = true; this.getFullPath = () => { - return `/app/kibana#${VisualizeConstants.EDIT_PATH}/${this.id}`; + return `/app/kibana#/visualize/edit/${this.id}`; }; } } diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.test.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.test.ts similarity index 77% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.test.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.test.ts index ed0f6dc429ef4..d1def09978dbb 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.test.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.test.ts @@ -18,19 +18,24 @@ */ import { findListItems } from './find_list_items'; +import { coreMock } from '../../../../../../../core/public/mocks'; +import { SavedObjectsClientContract } from '../../../../../../../core/public'; +import { VisTypeAlias } from '../vis_types'; describe('saved_visualizations', () => { function testProps() { + const savedObjects = coreMock.createStart().savedObjects.client as jest.Mocked< + SavedObjectsClientContract + >; + (savedObjects.find as jest.Mock).mockImplementation(() => ({ + total: 0, + savedObjects: [], + })); return { visTypes: [], search: '', size: 10, - savedObjectsClient: { - find: jest.fn(async () => ({ - total: 0, - savedObjects: [], - })), - }, + savedObjectsClient: savedObjects, mapSavedObjectApiHits: jest.fn(), }; } @@ -60,7 +65,7 @@ describe('saved_visualizations', () => { searchFields: ['baz', 'bing'], }, }, - }, + } as VisTypeAlias, ], }; const { find } = props.savedObjectsClient; @@ -86,7 +91,7 @@ describe('saved_visualizations', () => { searchFields: ['baz', 'bing', 'barfield'], }, }, - }, + } as VisTypeAlias, { appExtensions: { visualizations: { @@ -94,7 +99,7 @@ describe('saved_visualizations', () => { searchFields: ['baz', 'bing', 'foofield'], }, }, - }, + } as VisTypeAlias, ], }; const { find } = props.savedObjectsClient; @@ -128,24 +133,11 @@ describe('saved_visualizations', () => { it('uses type-specific toListItem function, if available', async () => { const props = { ...testProps(), - savedObjectsClient: { - find: jest.fn(async () => ({ - total: 2, - savedObjects: [ - { - id: 'lotr', - type: 'wizard', - attributes: { label: 'Gandalf' }, - }, - { - id: 'wat', - type: 'visualization', - attributes: { title: 'WATEVER' }, - }, - ], - })), - }, - mapSavedObjectApiHits(savedObject) { + mapSavedObjectApiHits(savedObject: { + id: string; + type: string; + attributes: { title: string }; + }) { return { id: savedObject.id, title: `DEFAULT ${savedObject.attributes.title}`, @@ -159,14 +151,31 @@ describe('saved_visualizations', () => { toListItem(savedObject) { return { id: savedObject.id, - title: `${savedObject.attributes.label} THE GRAY`, + title: `${(savedObject.attributes as { label: string }).label} THE GRAY`, }; }, }, }, - }, + } as VisTypeAlias, ], }; + + (props.savedObjectsClient.find as jest.Mock).mockImplementationOnce(async () => ({ + total: 2, + savedObjects: [ + { + id: 'lotr', + type: 'wizard', + attributes: { label: 'Gandalf' }, + }, + { + id: 'wat', + type: 'visualization', + attributes: { title: 'WATEVER' }, + }, + ], + })); + const items = await findListItems(props); expect(items).toEqual({ total: 2, diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.ts similarity index 64% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.ts index a7fcee67adf72..02db90a762e89 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/find_list_items.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/find_list_items.ts @@ -18,6 +18,13 @@ */ import _ from 'lodash'; +import { + SavedObjectAttributes, + SavedObjectsClientContract, +} from '../../../../../../../core/public'; +import { SavedObjectLoader } from '../../../../../../../plugins/saved_objects/public'; +import { VisTypeAlias } from '../vis_types'; +import { VisualizationsAppExtension } from '../vis_types/vis_type_alias_registry'; /** * Search for visualizations and convert them into a list display-friendly format. @@ -28,34 +35,42 @@ export async function findListItems({ size, savedObjectsClient, mapSavedObjectApiHits, +}: { + search: string; + size: number; + visTypes: VisTypeAlias[]; + savedObjectsClient: SavedObjectsClientContract; + mapSavedObjectApiHits: SavedObjectLoader['mapSavedObjectApiHits']; }) { - const extensions = _.compact( - visTypes.map(v => v.appExtensions && v.appExtensions.visualizations) - ); + const extensions = visTypes + .map(v => v.appExtensions?.visualizations) + .filter(Boolean) as VisualizationsAppExtension[]; const extensionByType = extensions.reduce((acc, m) => { - return m.docTypes.reduce((_acc, type) => { + return m!.docTypes.reduce((_acc, type) => { acc[type] = m; return acc; }, acc); - }, {}); - const searchOption = (field, ...defaults) => + }, {} as { [visType: string]: VisualizationsAppExtension }); + const searchOption = (field: string, ...defaults: string[]) => _(extensions) .pluck(field) .concat(defaults) .compact() .flatten() .uniq() - .value(); + .value() as string[]; const searchOptions = { type: searchOption('docTypes', 'visualization'), searchFields: searchOption('searchFields', 'title^3', 'description'), search: search ? `${search}*` : undefined, perPage: size, page: 1, - defaultSearchOperator: 'AND', + defaultSearchOperator: 'AND' as 'AND', }; - const { total, savedObjects } = await savedObjectsClient.find(searchOptions); + const { total, savedObjects } = await savedObjectsClient.find( + searchOptions + ); return { total, diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/index.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/index.ts diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.test.ts similarity index 96% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.test.ts index 6549b317d1634..98af6d99025c2 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.test.ts @@ -18,7 +18,7 @@ */ import { extractReferences, injectReferences } from './saved_visualization_references'; -import { VisSavedObject } from '../embeddable/visualize_embeddable'; +import { VisSavedObject, VisState } from '../types'; describe('extractReferences', () => { test('extracts nothing if savedSearchId is empty', () => { @@ -128,7 +128,7 @@ Object { id: '1', title: 'test', savedSearchRefName: 'search_0', - visState: { + visState: ({ params: { controls: [ { @@ -140,7 +140,7 @@ Object { }, ], }, - }, + } as unknown) as VisState, } as VisSavedObject; const references = [ { @@ -192,7 +192,7 @@ Object { const context = { id: '1', title: 'test', - visState: { + visState: ({ params: { controls: [ { @@ -201,7 +201,7 @@ Object { }, ], }, - }, + } as unknown) as VisState, } as VisSavedObject; expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( `"Could not find index pattern reference \\"control_0_index_pattern\\""` diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.ts similarity index 95% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.ts index 330f5e2dacd10..b995d340d44d9 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualization_references.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualization_references.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; -import { VisSavedObject } from '../embeddable/visualize_embeddable'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../../../../../core/public'; +import { VisSavedObject } from '../types'; export function extractReferences({ attributes, diff --git a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualizations.ts similarity index 91% rename from src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualizations.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualizations.ts index 7d0d6a10ff66f..fc0f77d54059c 100644 --- a/src/legacy/core_plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/saved_visualizations.ts @@ -19,18 +19,15 @@ import { SavedObjectLoader, SavedObjectKibanaServices, -} from '../../../../../plugins/saved_objects/public'; - -// @ts-ignore +} from '../../../../../../../plugins/saved_objects/public'; import { findListItems } from './find_list_items'; import { createSavedVisClass } from './_saved_vis'; -import { createVisualizeEditUrl } from '../../../kibana/public/visualize'; -import { TypesStart } from '../np_ready/public/types'; +import { TypesStart } from '../vis_types'; export interface SavedObjectKibanaServicesWithVisualizations extends SavedObjectKibanaServices { visualizationTypes: TypesStart; } - +export type SavedVisualizationsLoader = ReturnType; export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisualizations) { const { savedObjectsClient, visualizationTypes } = services; @@ -59,7 +56,7 @@ export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisu source.icon = source.type.icon; source.image = source.type.image; source.typeTitle = source.type.title; - source.editUrl = `#${createVisualizeEditUrl(id)}`; + source.editUrl = `#/visualize/edit/${id}`; return source; }; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts index 433c5c7b6df0d..a977a4b452bf7 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts @@ -23,11 +23,18 @@ import { I18nStart, IUiSettingsClient, SavedObjectsStart, -} from 'src/core/public'; -import { TypesStart } from './types'; +} from '../../../../../../core/public'; +import { TypesStart } from './vis_types'; import { createGetterSetter } from '../../../../../../plugins/kibana_utils/public'; -import { FilterManager, IndexPatternsContract } from '../../../../../../plugins/data/public'; +import { + FilterManager, + IndexPatternsContract, + TimefilterContract, +} from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; +import { ExpressionsStart } from '../../../../../../plugins/expressions/public'; +import { UiActionsStart } from '../../../../../../plugins/ui_actions/public'; +import { SavedVisualizationsLoader } from './saved_visualizations'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -47,6 +54,8 @@ export const [getFilterManager, setFilterManager] = createGetterSetter('TimeFilter'); + export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'IndexPatterns' ); @@ -54,3 +63,11 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'UsageCollection' ); + +export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); + +export const [getUiActions, setUiActions] = createGetterSetter('UiActions'); + +export const [getSavedVisualizationsLoader, setSavedVisualizationsLoader] = createGetterSetter< + SavedVisualizationsLoader +>('SavedVisualisationsLoader'); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/types.ts new file mode 100644 index 0000000000000..d2ca4ffb92eb2 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types.ts @@ -0,0 +1,38 @@ +/* + * 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 { SavedObject } from '../../../../../../plugins/saved_objects/public'; +import { Vis, VisState, VisParams, VisualizationController } from './vis'; +import { ISearchSource } from '../../../../../../plugins/data/public/'; +import { SavedSearch } from '../../../../kibana/public/discover/np_ready/types'; + +export { Vis, VisState, VisParams, VisualizationController }; + +export interface VisSavedObject extends SavedObject { + vis: Vis; + description?: string; + searchSource: ISearchSource; + title: string; + uiStateJSON?: string; + destroy: () => void; + savedSearchRefName?: string; + savedSearchId?: string; + savedSearch?: SavedSearch; + visState: VisState; +} diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts index 19375e25a9fb7..990f27dca7556 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts @@ -17,7 +17,7 @@ * under the License. */ -import { VisType } from './types'; +import { VisType } from './vis_types'; import { IAggConfigs } from '../../legacy_imports'; import { Status } from './legacy/update_status'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts index f9b7db5c02d93..62b68082e21f8 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts @@ -18,7 +18,7 @@ */ import { Vis, VisState, VisParams } from './vis'; -import { VisType } from './types'; +import { VisType } from './vis_types'; import { IIndexPattern } from '../../../../../../plugins/data/common'; type InitVisStateType = diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/base_vis_type.js similarity index 95% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/base_vis_type.js index 351acc48e2676..50ff74cfe9dd3 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/base_vis_type.js @@ -19,8 +19,6 @@ import _ from 'lodash'; -import { DefaultEditorController } from '../../../../../vis_default_editor/public'; - export class BaseVisType { constructor(opts = {}) { if (!opts.name) { @@ -47,7 +45,7 @@ export class BaseVisType { }, requestHandler: 'courier', // select one from registry or pass a function responseHandler: 'none', - editor: DefaultEditorController, + editor: null, // no default is provided editorConfig: { collections: {}, // collections used for configuration (list of positions, ...) }, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/index.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/index.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/react_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/react_vis_type.js similarity index 100% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/react_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/react_vis_type.js diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/types_service.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/types_service.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/vis_type_alias_registry.ts similarity index 97% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/vis_type_alias_registry.ts index 97f4798c296d4..12b02ee9e6b32 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/vis_type_alias_registry.ts @@ -27,7 +27,7 @@ interface VisualizationListItem { typeTitle: string; } -interface VisualizationsAppExtension { +export interface VisualizationsAppExtension { docTypes: string[]; searchFields?: string[]; toListItem: (savedObject: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx index 58f7bc49d1cdb..2712019e42609 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx @@ -19,9 +19,9 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { TypesStart, VisType } from '../types'; +import { TypesStart, VisType } from '../vis_types'; import { NewVisModal } from './new_vis_modal'; -import { SavedObjectsStart } from 'kibana/public'; +import { SavedObjectsStart } from '../../../../../../../core/public'; describe('NewVisModal', () => { const { location } = window; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx index b39e8e8926707..7c10001eddb50 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx @@ -23,10 +23,10 @@ import { EuiModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; -import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; +import { IUiSettingsClient, SavedObjectsStart } from '../../../../../../../core/public'; import { SearchSelection } from './search_selection'; import { TypeSelection } from './type_selection'; -import { TypesStart, VisType, VisTypeAlias } from '../types'; +import { TypesStart, VisType, VisTypeAlias } from '../vis_types'; import { UsageCollectionSetup } from '../../../../../../../plugins/usage_collection/public'; interface TypeSelectionProps { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx index 9b3b8a6425e52..f8eb191dd5f92 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/search_selection/search_selection.tsx @@ -21,10 +21,10 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui' import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; +import { IUiSettingsClient, SavedObjectsStart } from '../../../../../../../../core/public'; import { SavedObjectFinderUi } from '../../../../../../../../plugins/saved_objects/public'; -import { VisType } from '../../types'; +import { VisType } from '../../vis_types'; interface SearchSelectionProps { onSearchSelected: (searchId: string, searchType: string) => void; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx index 5068f43952c4e..e84314853ba4c 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx @@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { EuiText, EuiButton } from '@elastic/eui'; import { VisTypeAliasListEntry } from './type_selection'; -import { VisTypeAlias } from '../../types'; +import { VisTypeAlias } from '../../vis_types'; interface Props { promotedTypes: VisTypeAliasListEntry[]; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx index 574f5b3cccc99..81dcecfee2613 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx @@ -40,7 +40,7 @@ import { VisTypeAlias } from '../../../../../../visualizations/public'; import { NewVisHelp } from './new_vis_help'; import { VisHelpText } from './vis_help_text'; import { VisTypeIcon } from './vis_type_icon'; -import { VisType, TypesStart } from '../../types'; +import { VisType, TypesStart } from '../../vis_types'; export interface VisTypeListEntry extends VisType { highlighted: boolean; diff --git a/src/legacy/ui/public/notify/index.js b/src/legacy/ui/public/notify/index.js index f7526f3b8f8fd..7ec6a394d7e88 100644 --- a/src/legacy/ui/public/notify/index.js +++ b/src/legacy/ui/public/notify/index.js @@ -19,5 +19,4 @@ export { fatalError, addFatalErrorCallback } from './fatal_error'; export { toastNotifications } from './toasts'; -export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect'; export { banners } from './banners'; diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index e1fe7d414756a..7600bd9db6094 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -23,12 +23,6 @@ export interface IKibanaSearchResponse { */ id?: string; - /** - * If relevant to the search strategy, return a percentage - * that represents how progress is indicated. - */ - percentComplete?: number; - /** * If relevant to the search strategy, return a total number * that represents how progress is indicated. diff --git a/src/plugins/data/public/search/es_search/es_search_strategy.ts b/src/plugins/data/public/search/es_search/es_search_strategy.ts index a5eab20a89f53..5382a59123e78 100644 --- a/src/plugins/data/public/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/public/search/es_search/es_search_strategy.ts @@ -26,6 +26,8 @@ import { ISearchContext, TSearchStrategyProvider, ISearchStrategy } from '../typ export const esSearchStrategyProvider: TSearchStrategyProvider = ( context: ISearchContext ): ISearchStrategy => { + const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY); + const { search } = syncStrategyProvider(context); return { search: (request, options) => { if (typeof request.params.preference === 'undefined') { @@ -33,11 +35,9 @@ export const esSearchStrategyProvider: TSearchStrategyProvider; + return search({ ...request, serverStrategy: ES_SEARCH_STRATEGY }, options) as Observable< + IEsSearchResponse + >; }, }; }; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 762c278a05640..853dbd09e1f93 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -35,7 +35,7 @@ export { export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; -export { SYNC_SEARCH_STRATEGY } from './sync_search_strategy'; +export { ISyncSearchRequest, SYNC_SEARCH_STRATEGY } from './sync_search_strategy'; export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; diff --git a/src/plugins/data/public/search/sync_search_strategy.test.ts b/src/plugins/data/public/search/sync_search_strategy.test.ts index 9378e5833f8d7..31a1adfa01c75 100644 --- a/src/plugins/data/public/search/sync_search_strategy.test.ts +++ b/src/plugins/data/public/search/sync_search_strategy.test.ts @@ -35,12 +35,9 @@ describe('Sync search strategy', () => { core: mockCoreStart, getSearchStrategy: jest.fn(), }); - syncSearch.search( - { - serverStrategy: SYNC_SEARCH_STRATEGY, - }, - {} - ); + const request = { serverStrategy: SYNC_SEARCH_STRATEGY }; + syncSearch.search(request, {}); + expect(mockCoreStart.http.fetch.mock.calls[0][0]).toEqual({ path: `/internal/search/${SYNC_SEARCH_STRATEGY}`, body: JSON.stringify({ @@ -50,4 +47,47 @@ describe('Sync search strategy', () => { signal: undefined, }); }); + + it('increments and decrements loading count on success', async () => { + const expectedLoadingCountValues = [0, 1, 0]; + const receivedLoadingCountValues: number[] = []; + + mockCoreStart.http.fetch.mockResolvedValueOnce('response'); + + const syncSearch = syncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn(), + }); + const request = { serverStrategy: SYNC_SEARCH_STRATEGY }; + + const loadingCount$ = mockCoreStart.http.addLoadingCountSource.mock.calls[0][0]; + loadingCount$.subscribe(value => receivedLoadingCountValues.push(value)); + + await syncSearch.search(request, {}).toPromise(); + + expect(receivedLoadingCountValues).toEqual(expectedLoadingCountValues); + }); + + it('increments and decrements loading count on failure', async () => { + expect.assertions(1); + const expectedLoadingCountValues = [0, 1, 0]; + const receivedLoadingCountValues: number[] = []; + + mockCoreStart.http.fetch.mockRejectedValueOnce('error'); + + const syncSearch = syncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn(), + }); + const request = { serverStrategy: SYNC_SEARCH_STRATEGY }; + + const loadingCount$ = mockCoreStart.http.addLoadingCountSource.mock.calls[0][0]; + loadingCount$.subscribe(value => receivedLoadingCountValues.push(value)); + + try { + await syncSearch.search(request, {}).toPromise(); + } catch (e) { + expect(receivedLoadingCountValues).toEqual(expectedLoadingCountValues); + } + }); }); diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts index c2cc180af546e..860ce593ae217 100644 --- a/src/plugins/data/public/search/sync_search_strategy.ts +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -18,7 +18,8 @@ */ import { BehaviorSubject, from } from 'rxjs'; -import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../common/search'; +import { finalize } from 'rxjs/operators'; +import { IKibanaSearchRequest } from '../../common/search'; import { ISearch, ISearchOptions } from './i_search'; import { TSearchStrategyProvider, ISearchStrategy, ISearchContext } from './types'; @@ -40,16 +41,14 @@ export const syncSearchStrategyProvider: TSearchStrategyProvider { loadingCount$.next(loadingCount$.getValue() + 1); - const response: Promise = context.core.http.fetch({ - path: `/internal/search/${request.serverStrategy}`, - method: 'POST', - body: JSON.stringify(request), - signal: options.signal, - }); - - response.then(() => loadingCount$.next(loadingCount$.getValue() - 1)); - - return from(response); + return from( + context.core.http.fetch({ + path: `/internal/search/${request.serverStrategy}`, + method: 'POST', + body: JSON.stringify(request), + signal: options.signal, + }) + ).pipe(finalize(() => loadingCount$.next(loadingCount$.getValue() - 1))); }; return { search }; diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index f032890e98901..02a5e0921fe4f 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -23,6 +23,7 @@ import { IRouter } from 'kibana/server'; import { IFieldType, Filter } from '../index'; import { findIndexPatternById, getFieldByName } from '../index_patterns'; +import { getRequestAbortedSignal } from '../lib'; export function registerValueSuggestionsRoute(router: IRouter) { router.post( @@ -50,6 +51,7 @@ export function registerValueSuggestionsRoute(router: IRouter) { const { field: fieldName, query, boolFilter } = request.body; const { index } = request.params; const { dataClient } = context.core.elasticsearch; + const signal = getRequestAbortedSignal(request.events.aborted$); const autocompleteSearchOptions = { timeout: await uiSettings.get('kibana.autocompleteTimeout'), @@ -62,7 +64,7 @@ export function registerValueSuggestionsRoute(router: IRouter) { const body = await getBody(autocompleteSearchOptions, field || fieldName, query, boolFilter); try { - const result = await dataClient.callAsCurrentUser('search', { index, body }); + const result = await dataClient.callAsCurrentUser('search', { index, body }, { signal }); const buckets: any[] = get(result, 'aggregations.suggestions.buckets') || diff --git a/src/plugins/data/server/lib/get_request_aborted_signal.test.ts b/src/plugins/data/server/lib/get_request_aborted_signal.test.ts new file mode 100644 index 0000000000000..3c1e20dbcb158 --- /dev/null +++ b/src/plugins/data/server/lib/get_request_aborted_signal.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { Subject } from 'rxjs'; +import { getRequestAbortedSignal } from './get_request_aborted_signal'; + +describe('abortableRequestHandler', () => { + jest.useFakeTimers(); + + it('should call abort if disconnected', () => { + const abortedSubject = new Subject(); + const aborted$ = abortedSubject.asObservable(); + const onAborted = jest.fn(); + + const signal = getRequestAbortedSignal(aborted$); + signal.addEventListener('abort', onAborted); + + // Shouldn't be aborted or call onAborted prior to disconnecting + expect(signal.aborted).toBe(false); + expect(onAborted).not.toBeCalled(); + + abortedSubject.next(); + jest.runAllTimers(); + + // Should be aborted and call onAborted after disconnecting + expect(signal.aborted).toBe(true); + expect(onAborted).toBeCalled(); + }); +}); diff --git a/src/plugins/data/server/lib/get_request_aborted_signal.ts b/src/plugins/data/server/lib/get_request_aborted_signal.ts new file mode 100644 index 0000000000000..d1541f1df9384 --- /dev/null +++ b/src/plugins/data/server/lib/get_request_aborted_signal.ts @@ -0,0 +1,33 @@ +/* + * 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 { Observable } from 'rxjs'; +// @ts-ignore not typed +import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; + +/** + * A simple utility function that returns an `AbortSignal` corresponding to an `AbortController` + * which aborts when the given request is aborted. + * @param aborted$ The observable of abort events (usually `request.events.aborted$`) + */ +export function getRequestAbortedSignal(aborted$: Observable): AbortSignal { + const controller = new AbortController(); + aborted$.subscribe(() => controller.abort()); + return controller.signal; +} diff --git a/src/plugins/data/server/lib/index.ts b/src/plugins/data/server/lib/index.ts new file mode 100644 index 0000000000000..a2af456846e14 --- /dev/null +++ b/src/plugins/data/server/lib/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 { getRequestAbortedSignal } from './get_request_aborted_signal'; diff --git a/src/plugins/data/server/search/create_api.test.ts b/src/plugins/data/server/search/create_api.test.ts index cc13269e1aa21..99e48056ef857 100644 --- a/src/plugins/data/server/search/create_api.test.ts +++ b/src/plugins/data/server/search/create_api.test.ts @@ -25,7 +25,7 @@ import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; // let mockCoreSetup: MockedKeys; -const mockDefaultSearch = jest.fn(() => Promise.resolve({ percentComplete: 0 })); +const mockDefaultSearch = jest.fn(() => Promise.resolve({ total: 100, loaded: 0 })); const mockDefaultSearchStrategyProvider = jest.fn(() => Promise.resolve({ search: mockDefaultSearch, diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index e1613103ac399..798a4b82caaef 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -31,7 +31,7 @@ export function createApi({ }) { const api: IRouteHandlerSearchContext = { search: async (request, options, strategyName) => { - const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY; + const name = strategyName ?? DEFAULT_SEARCH_STRATEGY; const strategyProvider = searchStrategies[name]; if (!strategyProvider) { throw new Error(`No strategy found for ${strategyName}`); @@ -40,6 +40,15 @@ export function createApi({ const strategy = await strategyProvider(caller, api.search); return strategy.search(request, options); }, + cancel: async (id, strategyName) => { + const name = strategyName ?? DEFAULT_SEARCH_STRATEGY; + const strategyProvider = searchStrategies[name]; + if (!strategyProvider) { + throw new Error(`No strategy found for ${strategyName}`); + } + const strategy = await strategyProvider(caller, api.search); + return strategy.cancel && strategy.cancel(id); + }, }; return api; } diff --git a/src/plugins/data/server/search/i_route_handler_search_context.ts b/src/plugins/data/server/search/i_route_handler_search_context.ts index 8a44738a1dcfa..89862781b826e 100644 --- a/src/plugins/data/server/search/i_route_handler_search_context.ts +++ b/src/plugins/data/server/search/i_route_handler_search_context.ts @@ -17,8 +17,9 @@ * under the License. */ -import { ISearchGeneric } from './i_search'; +import { ISearchGeneric, ICancelGeneric } from './i_search'; export interface IRouteHandlerSearchContext { search: ISearchGeneric; + cancel: ICancelGeneric; } diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index 0a35734574153..ea014c5e136d9 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -42,7 +42,14 @@ export type ISearchGeneric = Promise; +export type ICancelGeneric = ( + id: string, + strategy?: T +) => Promise; + export type ISearch = ( request: IRequestTypesMap[T], options?: ISearchOptions ) => Promise; + +export type ICancel = (id: string) => Promise; diff --git a/src/plugins/data/server/search/i_search_strategy.ts b/src/plugins/data/server/search/i_search_strategy.ts index d00dd552c9e95..4cfc9608383a9 100644 --- a/src/plugins/data/server/search/i_search_strategy.ts +++ b/src/plugins/data/server/search/i_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; -import { ISearch, ISearchGeneric } from './i_search'; +import { ISearch, ICancel, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { ISearchContext } from './i_search_context'; @@ -28,6 +28,7 @@ import { ISearchContext } from './i_search_context'; */ export interface ISearchStrategy { search: ISearch; + cancel?: ICancel; } /** diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 6f726771c41b2..2b8c4b95ee022 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -19,6 +19,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../../../core/server'; +import { getRequestAbortedSignal } from '../lib'; export function registerSearchRoute(router: IRouter): void { router.post( @@ -34,9 +35,11 @@ export function registerSearchRoute(router: IRouter): void { }, async (context, request, res) => { const searchRequest = request.body; - const strategy = request.params.strategy; + const { strategy } = request.params; + const signal = getRequestAbortedSignal(request.events.aborted$); + try { - const response = await context.search!.search(searchRequest, {}, strategy); + const response = await context.search!.search(searchRequest, { signal }, strategy); return res.ok({ body: response }); } catch (err) { return res.customError({ @@ -51,4 +54,35 @@ export function registerSearchRoute(router: IRouter): void { } } ); + + router.delete( + { + path: '/internal/search/{strategy}/{id}', + validate: { + params: schema.object({ + strategy: schema.string(), + id: schema.string(), + }), + + query: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, res) => { + const { strategy, id } = request.params; + try { + await context.search!.cancel(id, strategy); + return res.ok(); + } catch (err) { + return res.customError({ + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + error: err.body.error, + }, + }, + }); + } + } + ); } diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 534e1cae3f62d..a1b332bb65617 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -22,6 +22,7 @@ import { Adapters } from '../types'; import { IContainer } from '../containers'; import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable'; import { ViewMode } from '../types'; +import { EmbeddableActionStorage } from './embeddable_action_storage'; function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title; @@ -49,6 +50,11 @@ export abstract class Embeddable< // TODO: Rename to destroyed. private destoyed: boolean = false; + private __actionStorage?: EmbeddableActionStorage; + public get actionStorage(): EmbeddableActionStorage { + return this.__actionStorage || (this.__actionStorage = new EmbeddableActionStorage(this)); + } + constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) { this.id = input.id; this.output = { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts new file mode 100644 index 0000000000000..56facc37fc666 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts @@ -0,0 +1,536 @@ +/* + * 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 { Embeddable } from './embeddable'; +import { EmbeddableInput } from './i_embeddable'; +import { ViewMode } from '../types'; +import { EmbeddableActionStorage, SerializedEvent } from './embeddable_action_storage'; +import { of } from '../../../../kibana_utils/common'; + +class TestEmbeddable extends Embeddable { + public readonly type = 'test'; + constructor() { + super({ id: 'test', viewMode: ViewMode.VIEW }, {}); + } + reload() {} +} + +describe('EmbeddableActionStorage', () => { + describe('.create()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.create).toBe('function'); + }); + + test('can add event to embeddable', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([]); + + await storage.create(event); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event]); + }); + + test('can create multiple events', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([]); + + await storage.create(event1); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event1]); + + await storage.create(event2); + await storage.create(event3); + + const events3 = embeddable.getInput().events || []; + expect(events3).toEqual([event1, event2, event3]); + }); + + test('throws when creating an event with the same ID', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const [, error] = await of(storage.create(event)); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[EEXIST]: Event with [eventId = EVENT_ID] already exists on [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + }); + + describe('.update()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.update).toBe('function'); + }); + + test('can update an existing event', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: { + name: 'foo', + } as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: { + name: 'bar', + } as any, + }; + + await storage.create(event1); + await storage.update(event2); + + const events = embeddable.getInput().events || []; + expect(events).toEqual([event2]); + }); + + test('updates event in place of the old event', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: { + name: 'foo', + } as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: { + name: 'bar', + } as any, + }; + const event22: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: { + name: 'baz', + } as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID', + action: { + name: 'qux', + } as any, + }; + + await storage.create(event1); + await storage.create(event2); + await storage.create(event3); + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([event1, event2, event3]); + + await storage.update(event22); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event1, event22, event3]); + + await storage.update(event2); + + const events3 = embeddable.getInput().events || []; + expect(events3).toEqual([event1, event2, event3]); + }); + + test('throws when updating event, but storage is empty', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + const [, error] = await of(storage.update(event)); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID] could not be updated as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('throws when updating event with ID that is not stored', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event1); + const [, error] = await of(storage.update(event2)); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID2] could not be updated as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + }); + + describe('.remove()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.remove).toBe('function'); + }); + + test('can remove existing event', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + await storage.remove(event.eventId); + + const events = embeddable.getInput().events || []; + expect(events).toEqual([]); + }); + + test('removes correct events in a list of events', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID', + action: { + name: 'foo', + } as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID', + action: { + name: 'bar', + } as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID', + action: { + name: 'qux', + } as any, + }; + + await storage.create(event1); + await storage.create(event2); + await storage.create(event3); + + const events1 = embeddable.getInput().events || []; + expect(events1).toEqual([event1, event2, event3]); + + await storage.remove(event2.eventId); + + const events2 = embeddable.getInput().events || []; + expect(events2).toEqual([event1, event3]); + + await storage.remove(event3.eventId); + + const events3 = embeddable.getInput().events || []; + expect(events3).toEqual([event1]); + + await storage.remove(event1.eventId); + + const events4 = embeddable.getInput().events || []; + expect(events4).toEqual([]); + }); + + test('throws when removing an event from an empty storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const [, error] = await of(storage.remove('EVENT_ID')); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID] could not be removed as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('throws when removing with ID that does not exist in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const [, error] = await of(storage.remove('WRONG_ID')); + await storage.remove(event.eventId); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = WRONG_ID] could not be removed as it does not exist in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + }); + + describe('.read()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.read).toBe('function'); + }); + + test('can read an existing event out of storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const event2 = await storage.read(event.eventId); + + expect(event2).toEqual(event); + }); + + test('throws when reading from empty storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const [, error] = await of(storage.read('EVENT_ID')); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = EVENT_ID] could not be found in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('throws when reading event with ID not existing in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event: SerializedEvent = { + eventId: 'EVENT_ID', + triggerId: 'TRIGGER-ID', + action: {} as any, + }; + + await storage.create(event); + const [, error] = await of(storage.read('WRONG_ID')); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toMatchInlineSnapshot( + `"[ENOENT]: Event with [eventId = WRONG_ID] could not be found in [embeddable.id = test, embeddable.title = undefined]."` + ); + }); + + test('returns correct event when multiple events are stored', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }; + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID2', + action: {} as any, + }; + const event3: SerializedEvent = { + eventId: 'EVENT_ID3', + triggerId: 'TRIGGER-ID3', + action: {} as any, + }; + + await storage.create(event1); + await storage.create(event2); + await storage.create(event3); + + const event12 = await storage.read(event1.eventId); + const event22 = await storage.read(event2.eventId); + const event32 = await storage.read(event3.eventId); + + expect(event12).toEqual(event1); + expect(event22).toEqual(event2); + expect(event32).toEqual(event3); + + expect(event12).not.toEqual(event2); + }); + }); + + describe('.count()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.count).toBe('function'); + }); + + test('returns 0 when storage is empty', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const count = await storage.count(); + + expect(count).toBe(0); + }); + + test('returns correct number of events in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + expect(await storage.count()).toBe(0); + + await storage.create({ + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }); + + expect(await storage.count()).toBe(1); + + await storage.create({ + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }); + + expect(await storage.count()).toBe(2); + + await storage.remove('EVENT_ID1'); + + expect(await storage.count()).toBe(1); + + await storage.remove('EVENT_ID2'); + + expect(await storage.count()).toBe(0); + }); + }); + + describe('.list()', () => { + test('method exists', () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + expect(typeof storage.list).toBe('function'); + }); + + test('returns empty array when storage is empty', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const list = await storage.list(); + + expect(list).toEqual([]); + }); + + test('returns correct list of events in storage', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + + const event1: SerializedEvent = { + eventId: 'EVENT_ID1', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }; + + const event2: SerializedEvent = { + eventId: 'EVENT_ID2', + triggerId: 'TRIGGER-ID1', + action: {} as any, + }; + + expect(await storage.list()).toEqual([]); + + await storage.create(event1); + + expect(await storage.list()).toEqual([event1]); + + await storage.create(event2); + + expect(await storage.list()).toEqual([event1, event2]); + + await storage.remove('EVENT_ID1'); + + expect(await storage.list()).toEqual([event2]); + + await storage.remove('EVENT_ID2'); + + expect(await storage.list()).toEqual([]); + }); + }); +}); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts new file mode 100644 index 0000000000000..520f92840c5f9 --- /dev/null +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts @@ -0,0 +1,126 @@ +/* + * 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 { Embeddable } from '..'; + +/** + * Below two interfaces are here temporarily, they will move to `ui_actions` + * plugin once #58216 is merged. + */ +export interface SerializedEvent { + eventId: string; + triggerId: string; + action: unknown; +} +export interface ActionStorage { + create(event: SerializedEvent): Promise; + update(event: SerializedEvent): Promise; + remove(eventId: string): Promise; + read(eventId: string): Promise; + count(): Promise; + list(): Promise; +} + +export class EmbeddableActionStorage implements ActionStorage { + constructor(private readonly embbeddable: Embeddable) {} + + async create(event: SerializedEvent) { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const exists = !!events.find(({ eventId }) => eventId === event.eventId); + + if (exists) { + throw new Error( + `[EEXIST]: Event with [eventId = ${event.eventId}] already exists on ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + this.embbeddable.updateInput({ + ...input, + events: [...events, event], + }); + } + + async update(event: SerializedEvent) { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const index = events.findIndex(({ eventId }) => eventId === event.eventId); + + if (index === -1) { + throw new Error( + `[ENOENT]: Event with [eventId = ${event.eventId}] could not be ` + + `updated as it does not exist in ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + this.embbeddable.updateInput({ + ...input, + events: [...events.slice(0, index), event, ...events.slice(index + 1)], + }); + } + + async remove(eventId: string) { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const index = events.findIndex(event => eventId === event.eventId); + + if (index === -1) { + throw new Error( + `[ENOENT]: Event with [eventId = ${eventId}] could not be ` + + `removed as it does not exist in ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + this.embbeddable.updateInput({ + ...input, + events: [...events.slice(0, index), ...events.slice(index + 1)], + }); + } + + async read(eventId: string): Promise { + const input = this.embbeddable.getInput(); + const events = (input.events || []) as SerializedEvent[]; + const event = events.find(ev => eventId === ev.eventId); + + if (!event) { + throw new Error( + `[ENOENT]: Event with [eventId = ${eventId}] could not be found in ` + + `[embeddable.id = ${input.id}, embeddable.title = ${input.title}].` + ); + } + + return event; + } + + private __list() { + const input = this.embbeddable.getInput(); + return (input.events || []) as SerializedEvent[]; + } + + async count(): Promise { + return this.__list().length; + } + + async list(): Promise { + return this.__list(); + } +} diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 46cffab879684..62121cb0f23dd 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -30,6 +30,11 @@ export interface EmbeddableInput { hidePanelTitles?: boolean; isEmptyState?: boolean; + /** + * Reserved key for `ui_actions` events. + */ + events?: unknown; + /** * List of action IDs that this embeddable should not render. */ diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index f6ff9efca848b..4776204a8ab2f 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -630,6 +630,7 @@ describe('Execution', () => { }, }); expect(node2.debug?.rawError).toBeInstanceOf(Error); + expect(node2.debug?.rawError).toEqual(new Error('foo')); }); test('sets .debug object to expected shape', async () => { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 7e7df822724ae..f70a32f2f09c1 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -21,7 +21,7 @@ import { keys, last, mapValues, reduce, zipObject } from 'lodash'; import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; -import { Defer } from '../../../kibana_utils/common'; +import { Defer, now } from '../../../kibana_utils/common'; import { RequestAdapter, DataAdapter } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -211,11 +211,11 @@ export class Execution< // actually have a `then` function which would be treated as a `Promise`. const { resolvedArgs } = await this.resolveArgs(fn, input, fnArgs); args = resolvedArgs; - timeStart = this.params.debug ? performance.now() : 0; + timeStart = this.params.debug ? now() : 0; const output = await this.invokeFunction(fn, input, resolvedArgs); if (this.params.debug) { - const timeEnd: number = performance.now(); + const timeEnd: number = now(); (link as ExpressionAstFunction).debug = { success: true, fn, @@ -229,9 +229,9 @@ export class Execution< if (getType(output) === 'error') return output; input = output; } catch (rawError) { - const timeEnd: number = this.params.debug ? performance.now() : 0; - rawError.message = `[${fnName}] > ${rawError.message}`; + const timeEnd: number = this.params.debug ? now() : 0; const error = createError(rawError) as ExpressionValueError; + error.error.message = `[${fnName}] > ${error.error.message}`; if (this.params.debug) { (link as ExpressionAstFunction).debug = { diff --git a/src/legacy/ui/public/notify/app_redirect/app_redirect.test.js b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.test.ts similarity index 75% rename from src/legacy/ui/public/notify/app_redirect/app_redirect.test.js rename to src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.test.ts index a23aabe6ad88e..efb1393ff0b16 100644 --- a/src/legacy/ui/public/notify/app_redirect/app_redirect.test.js +++ b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.test.ts @@ -17,17 +17,12 @@ * under the License. */ +import { ILocationService } from 'angular'; +import { ToastsStart } from '../../../../../core/public'; import { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect'; let isToastAdded = false; - -jest.mock('../toasts', () => ({ - toastNotifications: { - addDanger: () => { - isToastAdded = true; - }, - }, -})); +const toasts: ToastsStart = {} as ToastsStart; describe('addAppRedirectMessageToUrl', () => { test('adds a message to the URL', () => { @@ -39,20 +34,29 @@ describe('addAppRedirectMessageToUrl', () => { describe('showAppRedirectNotification', () => { beforeEach(() => { isToastAdded = false; + toasts.addDanger = (): any => { + isToastAdded = true; + }; }); test(`adds a toast when there's a message in the URL`, () => { - showAppRedirectNotification({ - search: () => ({ app_redirect_message: 'redirect message' }), - }); + showAppRedirectNotification( + { + search: () => ({ app_redirect_message: 'redirect message' }), + } as ILocationService, + toasts + ); expect(isToastAdded).toBe(true); }); test(`doesn't add a toast when there's no message in the URL`, () => { - showAppRedirectNotification({ - search: () => ({ app_redirect_message: '' }), - }); + showAppRedirectNotification( + { + search: () => ({ app_redirect_message: '' }), + } as ILocationService, + toasts + ); expect(isToastAdded).toBe(false); }); diff --git a/src/legacy/ui/public/notify/app_redirect/app_redirect.js b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.ts similarity index 82% rename from src/legacy/ui/public/notify/app_redirect/app_redirect.js rename to src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.ts index a92e5401e5e75..e79ab4b2fbc6d 100644 --- a/src/legacy/ui/public/notify/app_redirect/app_redirect.js +++ b/src/plugins/kibana_legacy/public/notify/app_redirect/app_redirect.ts @@ -17,12 +17,13 @@ * under the License. */ +import { ILocationService } from 'angular'; import { modifyUrl } from '../../../../../core/utils'; -import { toastNotifications } from '../toasts'; +import { ToastsStart } from '../../../../../core/public'; const APP_REDIRECT_MESSAGE_PARAM = 'app_redirect_message'; -export function addAppRedirectMessageToUrl(url, message) { +export function addAppRedirectMessageToUrl(url: string, message: string) { return modifyUrl(url, urlParts => { urlParts.hash = modifyUrl(urlParts.hash || '', hashParts => { hashParts.query[APP_REDIRECT_MESSAGE_PARAM] = message; @@ -32,7 +33,7 @@ export function addAppRedirectMessageToUrl(url, message) { // If an app needs to redirect, e.g. due to an expired license, it can surface a message via // the URL query params. -export function showAppRedirectNotification($location) { +export function showAppRedirectNotification($location: ILocationService, toasts: ToastsStart) { const queryString = $location.search(); if (!queryString[APP_REDIRECT_MESSAGE_PARAM]) { @@ -42,5 +43,5 @@ export function showAppRedirectNotification($location) { const message = queryString[APP_REDIRECT_MESSAGE_PARAM]; $location.search(APP_REDIRECT_MESSAGE_PARAM, null); - toastNotifications.addDanger(message); + toasts.addDanger(message); } diff --git a/src/legacy/ui/public/notify/app_redirect/index.js b/src/plugins/kibana_legacy/public/notify/app_redirect/index.ts similarity index 100% rename from src/legacy/ui/public/notify/app_redirect/index.js rename to src/plugins/kibana_legacy/public/notify/app_redirect/index.ts diff --git a/src/plugins/kibana_legacy/public/notify/index.ts b/src/plugins/kibana_legacy/public/notify/index.ts index 6aa4e36ab7227..b6f29876c2737 100644 --- a/src/plugins/kibana_legacy/public/notify/index.ts +++ b/src/plugins/kibana_legacy/public/notify/index.ts @@ -18,3 +18,4 @@ */ export * from './toasts'; export * from './lib'; +export { addAppRedirectMessageToUrl, showAppRedirectNotification } from './app_redirect'; diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 3b07674315dce..50120edc0c056 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -25,3 +25,4 @@ export * from './typed_json'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; export { url } from './url'; +export { now } from './now'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js b/src/plugins/kibana_utils/common/now.ts similarity index 72% rename from src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js rename to src/plugins/kibana_utils/common/now.ts index 2e4ed0b36ed26..e6c6d74552622 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js +++ b/src/plugins/kibana_utils/common/now.ts @@ -18,13 +18,8 @@ */ /** - * Get the cluster info from the connected cluster. - * - * This is the equivalent to GET / - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch. + * Function that returns number in milliseconds since some undefined point in + * time. Use this function for performance measurements. */ -export function getClusterInfo(callCluster) { - return callCluster('info'); -} +export const now: () => number = + typeof performance === 'object' ? performance.now.bind(performance) : Date.now; diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index da70d9fe89525..275a9da96a2c4 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -39,7 +39,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../legacy/core_plugins/visualizations/public/embeddable/constants'; + +// TODO: can't import from '../../../../legacy/core_plugins/visualizations/public/' directly, +// because yarn build:types fails after trying to emit type declarations for whole visualizations plugin +// Bunch of errors like this: 'Return type of exported function has or is using private name 'SavedVis'' +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../legacy/core_plugins/visualizations/public/np_ready/public/embeddable/constants'; export interface OnSaveProps { newTitle: string; diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index 0ebcd52f1423c..0a49b0ae3084e 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -26,9 +26,18 @@ const mockSubtract = jest.fn().mockImplementation(() => { }; }); +const mockClone = jest.fn().mockImplementation(() => { + return { + clone: mockClone, + subtract: mockSubtract, + toISOString: jest.fn(), + }; +}); + jest.mock('moment', () => { return jest.fn().mockImplementation(() => { return { + clone: mockClone, subtract: mockSubtract, toISOString: jest.fn(), }; @@ -43,6 +52,7 @@ describe('TelemetryService', () => { expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { body: JSON.stringify({ unencrypted: false, timeRange: {} }), }); + expect(mockClone).toBeCalled(); expect(mockSubtract).toBeCalledWith(20, 'minutes'); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index 073886e7d1327..cb91451bd8ef4 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -92,7 +92,10 @@ export class TelemetryService { body: JSON.stringify({ unencrypted, timeRange: { - min: now.subtract(20, 'minutes').toISOString(), + min: now + .clone() // Need to clone it to avoid mutation (and max being the same value) + .subtract(20, 'minutes') + .toISOString(), max: now.toISOString(), }, }), diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index 1cb9f1490d442..ec8a48ca74911 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -63,6 +63,10 @@ export default function({ getService, getPageObjects }) { await filterBar.addFilter('bytes', 'is', '12345678'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); + // first round of requests sometimes times out, refresh all visualizations to fetch again + await queryBar.clickQuerySubmitButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); }); it('filters on pie charts', async () => { diff --git a/test/functional/apps/visualize/_linked_saved_searches.js b/test/functional/apps/visualize/_linked_saved_searches.ts similarity index 95% rename from test/functional/apps/visualize/_linked_saved_searches.js rename to test/functional/apps/visualize/_linked_saved_searches.ts index 37ec3f06f2ecd..345987a803394 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.js +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -18,8 +18,9 @@ */ import expect from '@kbn/expect'; - -export default function({ getService, getPageObjects }) { +import { FtrProviderContext } from '../../ftr_provider_context'; +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const retry = getService('retry'); const PageObjects = getPageObjects([ @@ -40,8 +41,6 @@ export default function({ getService, getPageObjects }) { await filterBar.addFilter('extension.raw', 'is', 'jpg'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.saveSearch(savedSearchName); - // TODO: Remove this once https://github.com/elastic/kibana/issues/19750 is properly resolved - await PageObjects.common.sleep(500); }); it('should create a visualization from a saved search', async () => { diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 1bd6358749e11..382543822d8ac 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -44,6 +44,7 @@ import { Browsers } from './browsers'; const throttleOption: string = process.env.TEST_THROTTLE_NETWORK as string; const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string; const remoteDebug: string = process.env.TEST_REMOTE_DEBUG as string; +const certValidation: string = process.env.NODE_TLS_REJECT_UNAUTHORIZED as string; const SECOND = 1000; const MINUTE = 60 * SECOND; const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit']; @@ -98,6 +99,9 @@ async function attemptToCreateCommand( // See: https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md chromeOptions.push('headless', 'disable-gpu'); } + if (certValidation === '0') { + chromeOptions.push('ignore-certificate-errors'); + } if (remoteDebug === '1') { // Visit chrome://inspect in chrome to remotely view/debug chromeOptions.push('headless', 'disable-gpu', 'remote-debugging-port=9222'); diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index 9f0a8ded649b2..b94558c209e6a 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -24,6 +24,7 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide const testSubjects = getService('testSubjects'); const queryBar = getService('queryBar'); const retry = getService('retry'); + const config = getService('config'); class SavedQueryManagementComponent { public async getCurrentlyLoadedQueryID() { @@ -177,7 +178,9 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide await retry.try(async () => { await testSubjects.click('saved-query-management-save-button'); - await testSubjects.existOrFail('saveQueryForm'); + await testSubjects.existOrFail('saveQueryForm', { + timeout: config.get('timeouts.waitForExists'), + }); }); } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b1cb9075434e8..bb084b3bb72a1 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -40,7 +40,7 @@ "xpack.taskManager": "legacy/plugins/task_manager", "xpack.transform": "legacy/plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", - "xpack.upgradeAssistant": "legacy/plugins/upgrade_assistant", + "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": "legacy/plugins/uptime", "xpack.watcher": "plugins/watcher" }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx index c5771995daa24..9213349a1492b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/PlatinumLicensePrompt.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { - EuiEmptyPrompt, EuiButton, - EuiPanel, + EuiEmptyPrompt, EuiFlexGroup, - EuiFlexItem + EuiFlexItem, + EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { invalidLicenseMessage } from '../../../../../../../plugins/apm/common/service_map'; import { useKibanaUrl } from '../../../hooks/useKibanaUrl'; export function PlatinumLicensePrompt() { @@ -43,14 +44,7 @@ export function PlatinumLicensePrompt() { )} ]} - body={ -

- {i18n.translate('xpack.apm.serviceMap.licensePromptBody', { - defaultMessage: - "In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data." - })} -

- } + body={

{invalidLicenseMessage}

} title={

{i18n.translate('xpack.apm.serviceMap.licensePromptTitle', { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 50ce918ea7037..3a6b4c5ebcaac 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -16,12 +16,7 @@ import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; -import { - asDuration, - asPercent, - toMicroseconds, - tpmUnit -} from '../../../../utils/formatters'; +import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; function LoadingSpinner() { return ( @@ -70,7 +65,7 @@ export function ServiceMetricList({ } ), description: isNumber(avgTransactionDuration) - ? asDuration(toMicroseconds(avgTransactionDuration, 'milliseconds')) + ? asDuration(avgTransactionDuration) : null }, { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 5fea4be9ca0da..b14ecaa803f6d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -17,12 +17,14 @@ import React, { useState } from 'react'; import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; +import { isValidPlatinumLicense } from '../../../../../../../plugins/apm/common/service_map'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useCallApmApi } from '../../../hooks/useCallApmApi'; import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity'; import { useLicense } from '../../../hooks/useLicense'; +import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { Controls } from './Controls'; @@ -31,7 +33,6 @@ import { getCytoscapeElements } from './get_cytoscape_elements'; import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; import { Popover } from './Popover'; import { useRefHeight } from './useRefHeight'; -import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; interface ServiceMapProps { serviceName?: string; @@ -195,13 +196,13 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [elements]); - const isValidPlatinumLicense = - license?.isActive && - (license?.type === 'platinum' || license?.type === 'trial'); - const [wrapperRef, height] = useRefHeight(); - return isValidPlatinumLicense ? ( + if (!license) { + return null; + } + + return isValidPlatinumLicense(license) ? (
showAppRedirectNotification($location, npStart.core.notifications.toasts)); /** * If there is a configured `kibana.defaultAppId`, and it is a dashboard ID, we'll diff --git a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js index 0ee4f76ebf9d0..67086883a9a32 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js +++ b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js @@ -268,6 +268,10 @@ export class JsonIndexFilePicker extends Component { maxFileSize: bytesToSize(MAX_FILE_SIZE), }} /> +
+ {i18n.translate('xpack.fileUpload.jsonIndexFilePicker.coordinateSystemAccepted', { + defaultMessage: 'Coordinates must be in EPSG:4326 coordinate reference system.', + })}{' '} ) } diff --git a/x-pack/legacy/plugins/graph/public/application.ts b/x-pack/legacy/plugins/graph/public/application.ts index 80a797b7f0724..7bd18f841b478 100644 --- a/x-pack/legacy/plugins/graph/public/application.ts +++ b/x-pack/legacy/plugins/graph/public/application.ts @@ -24,7 +24,6 @@ import { configureAppAngularModule, createTopNavDirective, createTopNavHelper, - addAppRedirectMessageToUrl, } from './legacy_imports'; // @ts-ignore import { initGraphApp } from './app'; @@ -37,6 +36,7 @@ import { checkLicense } from '../../../../plugins/graph/common/check_license'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; import { createSavedWorkspacesLoader } from './services/persistence/saved_workspace_loader'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { addAppRedirectMessageToUrl } from '../../../../../src/plugins/kibana_legacy/public'; /** * These are dependencies of the Graph app besides the base dependencies diff --git a/x-pack/legacy/plugins/graph/public/legacy_imports.ts b/x-pack/legacy/plugins/graph/public/legacy_imports.ts index ac518d34551db..84fafdb580abe 100644 --- a/x-pack/legacy/plugins/graph/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/graph/public/legacy_imports.ts @@ -8,6 +8,4 @@ import 'ace'; // @ts-ignore export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; -// @ts-ignore -export { addAppRedirectMessageToUrl } from 'ui/notify'; export { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap index 486a830d21b65..b5fe334f8415e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap @@ -17,25 +17,6 @@ exports[`TooltipHeader multiple features, multiple layers: locked should show pa pageCount={3} /> - - - - - @@ -139,25 +120,6 @@ exports[`TooltipHeader multiple features, single layer: locked should show pagin pageCount={2} /> - - - - - 1) { + if (!isLocked && filteredFeatures.length > 1) { headerItems.push( diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/index.ts b/x-pack/legacy/plugins/ml/common/constants/calendars.ts similarity index 86% rename from x-pack/legacy/plugins/upgrade_assistant/server/index.ts rename to x-pack/legacy/plugins/ml/common/constants/calendars.ts index 8b0704283509d..1a56257ca1304 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/index.ts +++ b/x-pack/legacy/plugins/ml/common/constants/calendars.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { plugin } from './np_ready'; +export const GLOBAL_CALENDAR = '_all'; diff --git a/x-pack/legacy/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/legacy/plugins/ml/common/types/file_datavisualizer.ts new file mode 100644 index 0000000000000..bc03f82673a1f --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/file_datavisualizer.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface FindFileStructureResponse { + charset: string; + has_header_row: boolean; + has_byte_order_marker: boolean; + format: string; + field_stats: { + [fieldName: string]: { + count: number; + cardinality: number; + top_hits: Array<{ count: number; value: any }>; + }; + }; + sample_start: string; + num_messages_analyzed: number; + mappings: { + [fieldName: string]: { + type: string; + }; + }; + quote: string; + delimiter: string; + need_client_timezone: boolean; + num_lines_analyzed: number; + column_names: string[]; +} diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx index 24cbfbfb346dd..add27193deb77 100644 --- a/x-pack/legacy/plugins/ml/public/application/app.tsx +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -12,6 +12,7 @@ import 'ace'; import { AppMountParameters, CoreStart } from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { SecurityPluginSetup } from '../../../../../plugins/security/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { setDependencyCache, clearCache } from './util/dependency_cache'; @@ -20,6 +21,7 @@ import { MlRouter } from './routing'; export interface MlDependencies extends AppMountParameters { data: DataPublicPluginStart; + security: SecurityPluginSetup; __LEGACY: { XSRF: string; APP_URL: string; @@ -49,6 +51,7 @@ const App: FC = ({ coreStart, deps }) => { APP_URL: deps.__LEGACY.APP_URL, application: coreStart.application, http: coreStart.http, + security: deps.security, }); deps.onAppLeave(actions => { clearCache(); @@ -64,6 +67,7 @@ const App: FC = ({ coreStart, deps }) => { const services = { appName: 'ML', data: deps.data, + security: deps.security, ...coreStart, }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts index aaf539322809b..5fcd7c5473d3b 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -10,9 +10,11 @@ import { useKibana, KibanaReactContextValue, } from '../../../../../../../../src/plugins/kibana_react/public'; +import { SecurityPluginSetup } from '../../../../../../../plugins/security/public'; interface StartPlugins { data: DataPublicPluginStart; + security: SecurityPluginSetup; } export type StartServices = CoreStart & StartPlugins; // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config.ts new file mode 100644 index 0000000000000..3344cdf991e6b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; + +export function createFilebeatConfig( + index: string, + results: FindFileStructureResponse, + ingestPipelineId: string, + username: string | null +) { + return [ + 'filebeat.inputs:', + '- type: log', + ...getPaths(), + ...getEncoding(results), + ...getExcludeLines(results), + ...getMultiline(results), + '', + ...getProcessors(results), + 'output.elasticsearch:', + ' hosts: [""]', + ...getUserDetails(username), + ` index: "${index}"`, + ` pipeline: "${ingestPipelineId}"`, + '', + 'setup:', + ' template.enabled: false', + ' ilm.enabled: false', + ].join('\n'); +} + +function getPaths() { + const txt = i18n.translate('xpack.ml.fileDatavisualizer.fileBeatConfig.paths', { + defaultMessage: 'add path to your files here', + }); + return [' paths:', ` - '<${txt}>'`]; +} + +function getEncoding(results: any) { + return results.charset !== 'UTF-8' ? [` encoding: ${results.charset}`] : []; +} + +function getExcludeLines(results: any) { + return results.exclude_lines_pattern !== undefined + ? [` exclude_lines: ['${results.exclude_lines_pattern.replace(/'/g, "''")}']`] + : []; +} + +function getMultiline(results: any) { + return results.multiline_start_pattern !== undefined + ? [ + ' multiline:', + ` pattern: '${results.multiline_start_pattern.replace(/'/g, "''")}'`, + ' match: after', + ' negate: true', + ] + : []; +} + +function getProcessors(results: any) { + return results.need_client_timezone === true ? ['processors:', '- add_locale: ~', ''] : []; +} + +function getUserDetails(username: string | null) { + return username !== null ? [` username: "${username}"`, ' password: ""'] : []; +} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx new file mode 100644 index 0000000000000..30fc74acbabf4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx @@ -0,0 +1,162 @@ +/* + * 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. + */ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiSpacer, + EuiCodeBlock, + EuiCode, + EuiCopy, +} from '@elastic/eui'; +import { createFilebeatConfig } from './filebeat_config'; +import { useMlKibana } from '../../../../contexts/kibana'; +import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; + +export enum EDITOR_MODE { + HIDDEN, + READONLY, + EDITABLE, +} +interface Props { + index: string; + results: FindFileStructureResponse; + indexPatternId: string; + ingestPipelineId: string; + closeFlyout(): void; +} +export const FilebeatConfigFlyout: FC = ({ + index, + results, + indexPatternId, + ingestPipelineId, + closeFlyout, +}) => { + const [fileBeatConfig, setFileBeatConfig] = useState(''); + const [username, setUsername] = useState(null); + const { + services: { security }, + } = useMlKibana(); + + useEffect(() => { + security.authc.getCurrentUser().then(user => { + setUsername(user.username === undefined ? null : user.username); + }); + }, []); + + useEffect(() => { + const config = createFilebeatConfig(index, results, ingestPipelineId, username); + setFileBeatConfig(config); + }, [username]); + + return ( + + + + + + + + + + + + + + + + {copy => ( + + + + )} + + + + + + ); +}; + +const Contents: FC<{ + value: string; + index: string; + username: string | null; +}> = ({ value, index, username }) => { + return ( + + +
+ +
+
+ +

+ {index} }} + /> +

+

+ filebeat.yml }} + /> +

+ + + + {value} + + +

+ {username === null ? ( + {''}, + }} + /> + ) : ( + {username}, + password: {''}, + esUrl: {''}, + }} + /> + )} +

+
+ ); +}; diff --git a/x-pack/legacy/plugins/uptime/server/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/index.ts similarity index 70% rename from x-pack/legacy/plugins/uptime/server/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/index.ts index d063f0d8c2288..9286b92c2ab97 100644 --- a/x-pack/legacy/plugins/uptime/server/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { initServerWithKibana, KibanaServer } from './kibana.index'; -export { plugin } from './plugin'; +export { FilebeatConfigFlyout } from './filebeat_config_flyout'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index beb5918e277ae..bdfc27099a185 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -20,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { importerFactory } from './importer'; import { ResultsLinks } from '../results_links'; +import { FilebeatConfigFlyout } from '../filebeat_config_flyout'; import { ImportProgress, IMPORT_STATUS } from '../import_progress'; import { ImportErrors } from '../import_errors'; import { ImportSummary } from '../import_summary'; @@ -64,6 +65,7 @@ const DEFAULT_STATE = { indexNameError: '', indexPatternNameError: '', timeFieldName: undefined, + isFilebeatFlyoutVisible: false, }; export class ImportView extends Component { @@ -384,6 +386,16 @@ export class ImportView extends Component { }); }; + showFilebeatFlyout = () => { + this.setState({ isFilebeatFlyoutVisible: true }); + this.props.hideBottomBar(); + }; + + closeFilebeatFlyout = () => { + this.setState({ isFilebeatFlyoutVisible: false }); + this.props.showBottomBar(); + }; + async loadIndexNames() { const indices = await ml.getIndices(); const indexNames = indices.map(i => i.name); @@ -424,6 +436,7 @@ export class ImportView extends Component { indexNameError, indexPatternNameError, timeFieldName, + isFilebeatFlyoutVisible, } = this.state; const createPipeline = pipelineString !== ''; @@ -549,7 +562,18 @@ export class ImportView extends Component { indexPatternId={indexPatternId} timeFieldName={timeFieldName} createIndexPattern={createIndexPattern} + showFilebeatFlyout={this.showFilebeatFlyout} /> + + {isFilebeatFlyoutVisible && ( + + )} )} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js index 710fa49e64167..27899a58beed2 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js @@ -148,17 +148,17 @@ function populateFailures(error, failures, chunkCount) { } } -// The file structure endpoint sets the timezone to be {{ beat.timezone }} +// The file structure endpoint sets the timezone to be {{ event.timezone }} // as that's the variable Filebeat would send the client timezone in. // In this data import function the UI is effectively performing the role of Filebeat, // i.e. doing basic parsing, processing and conversion to JSON before forwarding to the ingest pipeline. // But it's not sending every single field that Filebeat would add, so the ingest pipeline -// cannot look for a beat.timezone variable in each input record. -// Therefore we need to replace {{ beat.timezone }} with the actual browser timezone +// cannot look for a event.timezone variable in each input record. +// Therefore we need to replace {{ event.timezone }} with the actual browser timezone function updatePipelineTimezone(ingestPipeline) { if (ingestPipeline !== undefined && ingestPipeline.processors && ingestPipeline.processors) { const dateProcessor = ingestPipeline.processors.find( - p => p.date !== undefined && p.date.timezone === '{{ beat.timezone }}' + p => p.date !== undefined && p.date.timezone === '{{ event.timezone }}' ); if (dateProcessor) { diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js index 840248817945a..c2d3ac69f0963 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/message_importer.js @@ -77,6 +77,11 @@ export class MessageImporter extends Importer { if (this.multilineStartRegex === null || line.match(this.multilineStartRegex) !== null) { this.addMessage(data, message); message = ''; + } else if (data.length === 0) { + // discard everything before the first line that is considered the first line of a message + // as it could be left over partial data from a spilt or rolled over log, + // or could be a blank line after the header in a csv file + return ''; } else { message += '\n'; } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js deleted file mode 100644 index aaebca2f58963..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js +++ /dev/null @@ -1,182 +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 { FormattedMessage } from '@kbn/i18n/react'; -import React, { Component } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; - -import moment from 'moment'; - -import { ml } from '../../../../services/ml_api_service'; -import { isFullLicense } from '../../../../license/check_license'; -import { checkPermission } from '../../../../privilege/check_privilege'; -import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; -import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; - -const RECHECK_DELAY_MS = 3000; - -class ResultsLinksUI extends Component { - constructor(props) { - super(props); - - this.state = { - from: 'now-30m', - to: 'now', - }; - - this.recheckTimeout = null; - this.showCreateJobLink = true; - } - - componentDidMount() { - this.showCreateJobLink = checkPermission('canCreateJob') && mlNodesAvailable(); - // if this data has a time field, - // find the start and end times - if (this.props.timeFieldName !== undefined) { - this.updateTimeValues(); - } - } - - componentWillUnmount() { - clearTimeout(this.recheckTimeout); - } - - async updateTimeValues(recheck = true) { - const { index, timeFieldName } = this.props; - - const { from, to } = await getFullTimeRange(index, timeFieldName); - this.setState({ - from: from === null ? this.state.from : from, - to: to === null ? this.state.to : to, - }); - - // these links may have been drawn too quickly for the index to be ready - // to give us the correct start and end times. - // especially if the data was small. - // so if the start and end were null, try again in 3s - // the timeout is cleared when this component unmounts. just in case the user - // resets the form or navigates away within 3s - if (recheck && (from === null || to === null)) { - this.recheckTimeout = setTimeout(() => { - this.updateTimeValues(false); - }, RECHECK_DELAY_MS); - } - } - - render() { - const { index, indexPatternId, timeFieldName, createIndexPattern } = this.props; - - const { from, to } = this.state; - - const _g = - this.props.timeFieldName !== undefined - ? `&_g=(time:(from:'${from}',mode:quick,to:'${to}'))` - : ''; - - const { basePath } = this.props.kibana.services.http; - return ( - - {createIndexPattern && ( - - } - title={ - - } - description="" - href={`${basePath.get()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${_g}`} - /> - - )} - - {isFullLicense() === true && - timeFieldName !== undefined && - this.showCreateJobLink && - createIndexPattern && ( - - } - title={ - - } - description="" - href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${_g}`} - /> - - )} - - {createIndexPattern && ( - - } - title={ - - } - description="" - href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${_g}`} - /> - - )} - - - } - title={ - - } - description="" - href={`${basePath.get()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} - /> - - - - } - title={ - - } - description="" - href={`${basePath.get()}/app/kibana#/management/kibana/index_patterns/${ - createIndexPattern ? indexPatternId : '' - }`} - /> - - - ); - } -} - -export const ResultsLinks = withKibana(ResultsLinksUI); - -async function getFullTimeRange(index, timeFieldName) { - const query = { bool: { must: [{ query_string: { analyze_wildcard: true, query: '*' } }] } }; - const resp = await ml.getTimeFieldRange({ - index, - timeFieldName, - query, - }); - - return { - from: moment(resp.start.epoch).toISOString(), - to: moment(resp.end.epoch).toISOString(), - }; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx new file mode 100644 index 0000000000000..debadba19051b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -0,0 +1,190 @@ +/* + * 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 moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; +import { ml } from '../../../../services/ml_api_service'; +import { isFullLicense } from '../../../../license/check_license'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; +import { useMlKibana } from '../../../../contexts/kibana'; + +const RECHECK_DELAY_MS = 3000; + +interface Props { + index: string; + indexPatternId: string; + timeFieldName?: string; + createIndexPattern: boolean; + showFilebeatFlyout(): void; +} + +export const ResultsLinks: FC = ({ + index, + indexPatternId, + timeFieldName, + createIndexPattern, + showFilebeatFlyout, +}) => { + const [duration, setDuration] = useState({ + from: 'now-30m', + to: 'now', + }); + const [showCreateJobLink, setShowCreateJobLink] = useState(false); + const [globalStateString, setGlobalStateString] = useState(''); + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); + + useEffect(() => { + setShowCreateJobLink(checkPermission('canCreateJob') && mlNodesAvailable()); + updateTimeValues(); + }, []); + + useEffect(() => { + const _g = + timeFieldName !== undefined + ? `&_g=(time:(from:'${duration.from}',mode:quick,to:'${duration.to}'))` + : ''; + setGlobalStateString(_g); + }, [duration]); + + async function updateTimeValues(recheck = true) { + if (timeFieldName !== undefined) { + const { from, to } = await getFullTimeRange(index, timeFieldName); + setDuration({ + from: from === null ? duration.from : from, + to: to === null ? duration.to : to, + }); + + // these links may have been drawn too quickly for the index to be ready + // to give us the correct start and end times. + // especially if the data was small. + // so if the start and end were null, try again in 3s + if (recheck && (from === null || to === null)) { + setTimeout(() => { + updateTimeValues(false); + }, RECHECK_DELAY_MS); + } + } + } + + return ( + + {createIndexPattern && ( + + } + title={ + + } + description="" + href={`${basePath.get()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${globalStateString}`} + /> + + )} + + {isFullLicense() === true && + timeFieldName !== undefined && + showCreateJobLink && + createIndexPattern && ( + + } + title={ + + } + description="" + href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${globalStateString}`} + /> + + )} + + {createIndexPattern && ( + + } + title={ + + } + description="" + href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${globalStateString}`} + /> + + )} + + + } + title={ + + } + description="" + href={`${basePath.get()}/app/kibana#/management/elasticsearch/index_management/indices/filter/${index}`} + /> + + + + } + title={ + + } + description="" + href={`${basePath.get()}/app/kibana#/management/kibana/index_patterns/${ + createIndexPattern ? indexPatternId : '' + }`} + /> + + + } + title={ + + } + description="" + onClick={showFilebeatFlyout} + /> + + + ); +}; + +async function getFullTimeRange(index: string, timeFieldName: string) { + const query = { bool: { must: [{ query_string: { analyze_wildcard: true, query: '*' } }] } }; + const resp = await ml.getTimeFieldRange({ + index, + timeFieldName, + query, + }); + + return { + from: moment(resp.start.epoch).toISOString(), + to: moment(resp.end.epoch).toISOString(), + }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 919972186761a..1e7327552623e 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -23,6 +23,7 @@ import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; import { ml } from '../../../../../../../../../services/ml_api_service'; import { Calendar } from '../../../../../../../../../../../common/types/calendars'; +import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars'; export const CalendarsSelection: FC = () => { const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); @@ -35,7 +36,9 @@ export const CalendarsSelection: FC = () => { async function loadCalendars() { setIsLoading(true); - const calendars = await ml.calendars(); + const calendars = (await ml.calendars()).filter( + c => c.job_ids.includes(GLOBAL_CALENDAR) === false + ); setOptions(calendars.map(c => ({ label: c.calendar_id, value: c }))); setSelectedOptions(selectedCalendars.map(c => ({ label: c.calendar_id, value: c }))); setIsLoading(false); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap index 2f5eb596a157b..21f505cff9aec 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap @@ -22,6 +22,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` eventsList={Array []} groupIds={Array []} isEdit={false} + isGlobalCalendar={false} isNewCalendarIdValid={true} jobIds={Array []} onCalendarIdChange={[Function]} @@ -30,6 +31,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` onDescriptionChange={[Function]} onEdit={[Function]} onEventDelete={[Function]} + onGlobalCalendarChange={[Function]} onGroupSelection={[Function]} onJobSelection={[Function]} saving={false} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index 0e7db62e44b51..acce01f1994db 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -84,59 +84,19 @@ exports[`CalendarForm Renders calendar form 1`] = ` value="" /> - - } - labelType="label" - > - - - + } - labelType="label" - > - - + name="switch" + /> diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js index fffcdf4c516f8..62daced72ceb2 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js @@ -18,6 +18,7 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiSwitch, } from '@elastic/eui'; import { EventsTable } from '../events_table'; @@ -68,6 +69,8 @@ export const CalendarForm = ({ selectedGroupOptions, selectedJobOptions, showNewEventModal, + isGlobalCalendar, + onGlobalCalendarChange, }) => { const msg = i18n.translate('xpack.ml.calendarsEdit.calendarForm.allowedCharactersDescription', { defaultMessage: @@ -81,7 +84,9 @@ export const CalendarForm = ({ return ( - {!isEdit && ( + {isEdit === true ? ( + + ) : (

@@ -128,39 +133,59 @@ export const CalendarForm = ({ )} - {isEdit && } - - } - > - - - + + } - > - - + checked={isGlobalCalendar} + onChange={onGlobalCalendarChange} + /> + + {isGlobalCalendar === false && ( + <> + + + + } + > + + + + + } + > + + + + )} @@ -240,4 +265,6 @@ CalendarForm.propTypes = { selectedGroupOptions: PropTypes.array.isRequired, selectedJobOptions: PropTypes.array.isRequired, showNewEventModal: PropTypes.func.isRequired, + isGlobalCalendar: PropTypes.bool.isRequired, + onGlobalCalendarChange: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 935e67ec05eff..815d1565d5bc4 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -19,6 +19,7 @@ import { NewEventModal } from './new_event_modal'; import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { GLOBAL_CALENDAR } from '../../../../../common/constants/calendars'; class NewCalendarUI extends Component { static propTypes = { @@ -46,6 +47,7 @@ class NewCalendarUI extends Component { events: [], saving: false, selectedCalendar: undefined, + isGlobalCalendar: false, }; } @@ -65,6 +67,7 @@ class NewCalendarUI extends Component { let eventsList = []; let selectedCalendar; let formCalendarId = ''; + let isGlobalCalendar = false; // Editing existing calendar. if (this.props.calendarId !== undefined) { @@ -74,13 +77,17 @@ class NewCalendarUI extends Component { formCalendarId = selectedCalendar.calendar_id; eventsList = selectedCalendar.events; - selectedCalendar.job_ids.forEach(id => { - if (jobIds.find(jobId => jobId === id)) { - selectedJobOptions.push({ label: id }); - } else if (groupIds.find(groupId => groupId === id)) { - selectedGroupOptions.push({ label: id }); - } - }); + if (selectedCalendar.job_ids.includes(GLOBAL_CALENDAR)) { + isGlobalCalendar = true; + } else { + selectedCalendar.job_ids.forEach(id => { + if (jobIds.find(jobId => jobId === id)) { + selectedJobOptions.push({ label: id }); + } else if (groupIds.find(groupId => groupId === id)) { + selectedGroupOptions.push({ label: id }); + } + }); + } } } @@ -96,6 +103,7 @@ class NewCalendarUI extends Component { selectedJobOptions, selectedGroupOptions, selectedCalendar, + isGlobalCalendar, }); } catch (error) { console.log(error); @@ -181,10 +189,15 @@ class NewCalendarUI extends Component { events, selectedGroupOptions, selectedJobOptions, + isGlobalCalendar, } = this.state; - const jobIds = selectedJobOptions.map(option => option.label); - const groupIds = selectedGroupOptions.map(option => option.label); + const allIds = isGlobalCalendar + ? [GLOBAL_CALENDAR] + : [ + ...selectedJobOptions.map(option => option.label), + ...selectedGroupOptions.map(option => option.label), + ]; // Reduce events to fields expected by api const eventsToSave = events.map(event => ({ @@ -198,7 +211,7 @@ class NewCalendarUI extends Component { calendarId: formCalendarId, description, events: eventsToSave, - job_ids: [...jobIds, ...groupIds], + job_ids: allIds, }; return calendar; @@ -214,6 +227,12 @@ class NewCalendarUI extends Component { })); }; + onGlobalCalendarChange = ({ currentTarget }) => { + this.setState({ + isGlobalCalendar: currentTarget.checked, + }); + }; + onJobSelection = selectedJobOptions => { this.setState({ selectedJobOptions, @@ -295,6 +314,7 @@ class NewCalendarUI extends Component { selectedCalendar, selectedJobOptions, selectedGroupOptions, + isGlobalCalendar, } = this.state; let modal = ''; @@ -351,6 +371,8 @@ class NewCalendarUI extends Component { selectedJobOptions={selectedJobOptions} onCreateGroupOption={this.onCreateGroupOption} showNewEventModal={this.showNewEventModal} + isGlobalCalendar={isGlobalCalendar} + onGlobalCalendarChange={this.onGlobalCalendarChange} /> {modal} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js index e4ab6677accf5..efc54c181fdc1 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js @@ -73,14 +73,17 @@ function getCalendars() { export function getCalendarSettingsData() { return new Promise(async (resolve, reject) => { try { - const data = await Promise.all([getJobIds(), getGroupIds(), getCalendars()]); + const [jobIds, groupIds, calendars] = await Promise.all([ + getJobIds(), + getGroupIds(), + getCalendars(), + ]); - const formattedData = { - jobIds: data[0], - groupIds: data[1], - calendars: data[2], - }; - resolve(formattedData); + resolve({ + jobIds, + groupIds, + calendars, + }); } catch (error) { console.log(error); reject(error); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index ff74c592b2b0f..14b65a04ce599 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -16,6 +16,7 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` Object { "field": "job_ids_string", "name": "Jobs", + "render": [Function], "sortable": true, "truncateText": true, }, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js index bd1dafcd6c0aa..be41eabd5ae2d 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -12,6 +12,8 @@ import { EuiButton, EuiLink, EuiInMemoryTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { GLOBAL_CALENDAR } from '../../../../../../common/constants/calendars'; + export const CalendarsListTable = ({ calendarsList, onDeleteClick, @@ -52,6 +54,18 @@ export const CalendarsListTable = ({ }), sortable: true, truncateText: true, + render: jobList => { + return jobList === GLOBAL_CALENDAR ? ( + + + + ) : ( + jobList + ); + }, }, { field: 'events_length', diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts index 8857485a58644..f837d90dba8fe 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -20,6 +20,7 @@ import { ChromeRecentlyAccessed, IBasePath, } from 'kibana/public'; +import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; export interface DependencyCache { timefilter: TimefilterSetup | null; @@ -38,6 +39,7 @@ export interface DependencyCache { APP_URL: string | null; application: ApplicationStart | null; http: HttpStart | null; + security: SecurityPluginSetup | null; } const cache: DependencyCache = { @@ -57,6 +59,7 @@ const cache: DependencyCache = { APP_URL: null, application: null, http: null, + security: null, }; export function setDependencyCache(deps: Partial) { @@ -189,6 +192,13 @@ export function getHttp() { return cache.http; } +export function getSecurity() { + if (cache.security === null) { + throw new Error("security hasn't been initialized"); + } + return cache.security; +} + export function clearCache() { console.log('clearing dependency cache'); // eslint-disable-line no-console Object.keys(cache).forEach(k => { diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts index bf431f0986d68..40a1afa06b5a6 100644 --- a/x-pack/legacy/plugins/ml/public/legacy.ts +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -6,14 +6,16 @@ import chrome from 'ui/chrome'; import { npSetup, npStart } from 'ui/new_platform'; - import { PluginInitializerContext } from 'src/core/public'; +import { SecurityPluginSetup } from '../../../../plugins/security/public'; + import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); export const setup = pluginInstance.setup(npSetup.core, { data: npStart.plugins.data, + security: ((npSetup.plugins as unknown) as { security: SecurityPluginSetup }).security, // security isn't in the PluginsSetup interface, but does exist __LEGACY: { XSRF: chrome.getXsrfToken(), // @ts-ignore getAppUrl is missing from chrome's definition diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts index 79af300bce4ec..cb39b31a32b14 100644 --- a/x-pack/legacy/plugins/ml/public/plugin.ts +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -8,7 +8,7 @@ import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; import { MlDependencies } from './application/app'; export class MlPlugin implements Plugin { - setup(core: CoreSetup, { data, __LEGACY }: MlDependencies) { + setup(core: CoreSetup, { data, security, __LEGACY }: MlDependencies) { core.application.register({ id: 'ml', title: 'Machine learning', @@ -21,6 +21,7 @@ export class MlPlugin implements Plugin { onAppLeave: params.onAppLeave, data, __LEGACY, + security, }); }, }); diff --git a/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js b/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js index 9317d3c6c3e07..e3092abb5d34e 100644 --- a/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js +++ b/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js @@ -450,6 +450,18 @@ export const elasticsearchJsPlugin = (Client, config, components) => { method: 'POST', }); + ml.records = ca({ + url: { + fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/records', + req: { + jobId: { + type: 'string', + }, + }, + }, + method: 'POST', + }); + ml.buckets = ca({ urls: [ { diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts index 19f2eda466179..488839f68b3fe 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts @@ -6,6 +6,8 @@ import Boom from 'boom'; +import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; + export interface CalendarEvent { calendar_id?: string; event_id?: string; @@ -32,7 +34,7 @@ export class EventManager { // jobId is optional async getAllEvents(jobId?: string) { - const calendarId = '_all'; + const calendarId = GLOBAL_CALENDAR; try { const resp = await this._client('ml.events', { calendarId, diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index fd5b5221393fc..9f30f609c60b6 100644 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'kibana/server'; +import { FindFileStructureResponse } from '../../../common/types/file_datavisualizer'; export type InputData = any[]; @@ -20,31 +21,7 @@ export type FormattedOverrides = InputOverrides & { }; export interface AnalysisResult { - results: { - charset: string; - has_header_row: boolean; - has_byte_order_marker: boolean; - format: string; - field_stats: { - [fieldName: string]: { - count: number; - cardinality: number; - top_hits: Array<{ count: number; value: any }>; - }; - }; - sample_start: string; - num_messages_analyzed: number; - mappings: { - [fieldName: string]: { - type: string; - }; - }; - quote: string; - delimiter: string; - need_client_timezone: boolean; - num_lines_analyzed: number; - column_names: string[]; - }; + results: FindFileStructureResponse; overrides?: FormattedOverrides; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js index 91f82f04a9a0c..6fbc071ef9854 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js @@ -5,6 +5,7 @@ */ import { CalendarManager } from '../calendar'; +import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export function groupsProvider(callWithRequest) { const calMngr = new CalendarManager(callWithRequest); @@ -12,11 +13,13 @@ export function groupsProvider(callWithRequest) { async function getAllGroups() { const groups = {}; const jobIds = {}; - const [JOBS, CALENDARS] = [0, 1]; - const results = await Promise.all([callWithRequest('ml.jobs'), calMngr.getAllCalendars()]); + const [{ jobs }, calendars] = await Promise.all([ + callWithRequest('ml.jobs'), + calMngr.getAllCalendars(), + ]); - if (results[JOBS] && results[JOBS].jobs) { - results[JOBS].jobs.forEach(job => { + if (jobs) { + jobs.forEach(job => { jobIds[job.job_id] = null; if (job.groups !== undefined) { job.groups.forEach(g => { @@ -33,10 +36,11 @@ export function groupsProvider(callWithRequest) { } }); } - if (results[CALENDARS]) { - results[CALENDARS].forEach(cal => { + if (calendars) { + calendars.forEach(cal => { cal.job_ids.forEach(jId => { - if (jobIds[jId] === undefined) { + // don't include _all in the calendar groups list + if (jId !== GLOBAL_CALENDAR && jobIds[jId] === undefined) { if (groups[jId] === undefined) { groups[jId] = { id: jId, diff --git a/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts index 927646e4f0acc..99dbdec9e945b 100644 --- a/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts @@ -376,11 +376,57 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }) ); + /** + * @apiGroup AnomalyDetectors + * + * @api {post} /api/ml/anomaly_detectors/:jobId/results/records Retrieves anomaly records for a job. + * @apiName GetRecords + * @apiDescription Retrieves anomaly records for a job. + * + * @apiParam {String} jobId Job ID. + * + * @apiSuccess {Number} count + * @apiSuccess {Object[]} records + */ + router.post( + { + path: '/api/ml/anomaly_detectors/{jobId}/results/records', + validate: { + params: schema.object({ + jobId: schema.string(), + }), + body: schema.object({ + desc: schema.maybe(schema.boolean()), + end: schema.maybe(schema.string()), + exclude_interim: schema.maybe(schema.boolean()), + 'page.from': schema.maybe(schema.number()), + 'page.size': schema.maybe(schema.number()), + record_score: schema.maybe(schema.number()), + sort: schema.maybe(schema.string()), + start: schema.maybe(schema.string()), + }), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.records', { + jobId: request.params.jobId, + ...request.body, + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup AnomalyDetectors * * @api {post} /api/ml/anomaly_detectors/:jobId/results/buckets Obtain bucket scores for the specified job ID - * @apiName GetOverallBuckets + * @apiName GetBuckets * @apiDescription The get buckets API presents a chronological view of the records, grouped by bucket. * * @apiParam {String} jobId Job ID. diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 946e3bd71d6c3..c5aa3e4d792fd 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -31,6 +31,8 @@ "DeleteAnomalyDetectorsJob", "ValidateAnomalyDetector", "ForecastAnomalyDetector", + "GetRecords", + "GetBuckets", "GetOverallBuckets", "GetCategories", "FileDataVisualizer", diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index 470642f9dd8a3..dcc7924fe171a 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -5,7 +5,7 @@ */ import sinon from 'sinon'; -import { addStackStats, getAllStats, handleAllStats } from './get_all_stats'; +import { getStackStats, getAllStats, handleAllStats } from './get_all_stats'; import { ESClusterStats } from './get_es_stats'; import { KibanaStats } from './get_kibana_stats'; import { ClustersHighLevelStats } from './get_high_level_stats'; @@ -223,7 +223,8 @@ describe('get_all_stats', () => { beats: {}, }); - expect(clusters).toStrictEqual(expectedClusters); + const [a, b, c] = expectedClusters; + expect(clusters).toStrictEqual([a, b, { ...c, stack_stats: {} }]); }); it('handles no clusters response', () => { @@ -233,9 +234,8 @@ describe('get_all_stats', () => { }); }); - describe('addStackStats', () => { + describe('getStackStats', () => { it('searches for clusters', () => { - const cluster = { cluster_uuid: 'a' }; const stats = { a: { count: 2, @@ -250,9 +250,7 @@ describe('get_all_stats', () => { }, }; - addStackStats(cluster as ESClusterStats, stats, 'xyz'); - - expect((cluster as any).stack_stats.xyz).toStrictEqual(stats.a); + expect(getStackStats('a', stats, 'xyz')).toStrictEqual({ xyz: stats.a }); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index aa5e937387daf..a6ed5254dabd5 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -61,38 +61,31 @@ export function handleAllStats( } ) { return clusters.map(cluster => { - // if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats - addStackStats(cluster, kibana, KIBANA_SYSTEM_ID); - addStackStats(cluster, logstash, LOGSTASH_SYSTEM_ID); - addStackStats(cluster, beats, BEATS_SYSTEM_ID); - mergeXPackStats(cluster, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph + const stats = { + ...cluster, + stack_stats: { + ...cluster.stack_stats, + // if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats + ...getStackStats(cluster.cluster_uuid, kibana, KIBANA_SYSTEM_ID), + ...getStackStats(cluster.cluster_uuid, logstash, LOGSTASH_SYSTEM_ID), + ...getStackStats(cluster.cluster_uuid, beats, BEATS_SYSTEM_ID), + }, + }; - return cluster; + mergeXPackStats(stats, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph + + return stats; }); } -/** - * Add product data to the {@code cluster}, only if it exists for the current {@code cluster}. - * - * @param {Object} cluster The current Elasticsearch cluster stats - * @param {Object} allProductStats Product stats, keyed by Cluster UUID - * @param {String} product The product name being added (e.g., 'kibana' or 'logstash') - */ -export function addStackStats( - cluster: ESClusterStats & { stack_stats?: { [product: string]: K } }, +export function getStackStats( + clusterUuid: string, allProductStats: T, product: string ) { - const productStats = allProductStats[cluster.cluster_uuid]; - + const productStats = allProductStats[clusterUuid]; // Don't add it if they're not using (or configured to report stats) this product for this cluster - if (productStats) { - if (!cluster.stack_stats) { - cluster.stack_stats = {}; - } - - cluster.stack_stats[product] = productStats; - } + return productStats ? { [product]: productStats } : {}; } export function mergeXPackStats( diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts index f0ae1163d3f52..2f2fffd3f0823 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts @@ -48,11 +48,6 @@ export function fetchElasticsearchStats( 'hits.hits._source.timestamp', 'hits.hits._source.cluster_name', 'hits.hits._source.version', - 'hits.hits._source.license.status', // license data only includes necessary fields to drive UI - 'hits.hits._source.license.type', - 'hits.hits._source.license.issue_date', - 'hits.hits._source.license.expiry_date', - 'hits.hits._source.license.expiry_date_in_millis', 'hits.hits._source.cluster_stats', 'hits.hits._source.stack_stats', ], @@ -79,7 +74,11 @@ export function fetchElasticsearchStats( export interface ESClusterStats { cluster_uuid: string; - type: 'cluster_stats'; + cluster_name: string; + timestamp: string; + version: string; + cluster_stats: object; + stack_stats?: object; } /** diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts new file mode 100644 index 0000000000000..bb8326ce0b63a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { getLicenses, handleLicenses, fetchLicenses } from './get_licenses'; + +describe('get_licenses', () => { + const callWith = sinon.stub(); + const size = 123; + const server = { + config: sinon.stub().returns({ + get: sinon + .stub() + .withArgs('xpack.monitoring.elasticsearch.index_pattern') + .returns('.monitoring-es-N-*') + .withArgs('xpack.monitoring.max_bucket_size') + .returns(size), + }), + }; + const response = { + hits: { + hits: [ + { _id: 'abc', _source: { cluster_uuid: 'abc', license: { type: 'basic' } } }, + { _id: 'xyz', _source: { cluster_uuid: 'xyz', license: { type: 'basic' } } }, + { _id: '123', _source: { cluster_uuid: '123' } }, + ], + }, + }; + const expectedClusters = response.hits.hits.map(hit => hit._source); + const clusterUuids = expectedClusters.map(cluster => ({ clusterUuid: cluster.cluster_uuid })); + const expectedLicenses = { + abc: { type: 'basic' }, + xyz: { type: 'basic' }, + '123': void 0, + }; + + describe('getLicenses', () => { + it('returns clusters', async () => { + callWith.withArgs('search').returns(Promise.resolve(response)); + + expect( + await getLicenses(clusterUuids, { server, callCluster: callWith } as any) + ).toStrictEqual(expectedLicenses); + }); + }); + + describe('fetchLicenses', () => { + it('searches for clusters', async () => { + callWith.returns(response); + + expect( + await fetchLicenses( + server, + callWith, + clusterUuids.map(({ clusterUuid }) => clusterUuid) + ) + ).toStrictEqual(response); + }); + }); + + describe('handleLicenses', () => { + // filterPath makes it easy to ignore anything unexpected because it will come back empty + it('handles unexpected response', () => { + const clusters = handleLicenses({} as any); + + expect(clusters).toStrictEqual({}); + }); + + it('handles valid response', () => { + const clusters = handleLicenses(response as any); + + expect(clusters).toStrictEqual(expectedLicenses); + }); + + it('handles no hits response', () => { + const clusters = handleLicenses({ hits: { hits: [] } } as any); + + expect(clusters).toStrictEqual({}); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.ts new file mode 100644 index 0000000000000..7364227e7dc92 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + StatsCollectionConfig, + LicenseGetter, +} from 'src/legacy/core_plugins/telemetry/server/collection_manager'; +import { SearchResponse } from 'elasticsearch'; +import { ESLicense } from 'src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; + +/** + * Get statistics for all selected Elasticsearch clusters. + */ +export const getLicenses: LicenseGetter = async (clustersDetails, { server, callCluster }) => { + const clusterUuids = clustersDetails.map(({ clusterUuid }) => clusterUuid); + const response = await fetchLicenses(server, callCluster, clusterUuids); + return handleLicenses(response); +}; + +/** + * Fetch the Elasticsearch stats. + * + * @param {Object} server The server instance + * @param {function} callCluster The callWithRequest or callWithInternalUser handler + * @param {Array} clusterUuids Cluster UUIDs to limit the request against + * + * Returns the response for the aggregations to fetch details for the product. + */ +export function fetchLicenses( + server: StatsCollectionConfig['server'], + callCluster: StatsCollectionConfig['callCluster'], + clusterUuids: string[] +) { + const config = server.config(); + const params = { + index: INDEX_PATTERN_ELASTICSEARCH, + size: config.get('monitoring.ui.max_bucket_size'), + ignoreUnavailable: true, + filterPath: ['hits.hits._source.cluster_uuid', 'hits.hits._source.license'], + body: { + query: { + bool: { + filter: [ + /* + * Note: Unlike most places, we don't care about the old _type: cluster_stats because it would NOT + * have the license in it (that used to be in the .monitoring-data-2 index in cluster_info) + */ + { term: { type: 'cluster_stats' } }, + { terms: { cluster_uuid: clusterUuids } }, + ], + }, + }, + collapse: { field: 'cluster_uuid' }, + sort: { timestamp: { order: 'desc' } }, + }, + }; + + return callCluster('search', params); +} + +export interface ESClusterStatsWithLicense { + cluster_uuid: string; + type: 'cluster_stats'; + license?: ESLicense; +} + +/** + * Extract the cluster stats for each cluster. + */ +export function handleLicenses(response: SearchResponse) { + const clusters = response.hits?.hits || []; + + return clusters.reduce( + (acc, { _source }) => ({ + ...acc, + [_source.cluster_uuid]: _source.license, + }), + {} + ); +} diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts index f0fda5229cb5c..0b14eb05f796f 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts @@ -7,6 +7,7 @@ import { telemetryCollectionManager } from '../../../../../../src/legacy/core_plugins/telemetry/server'; import { getAllStats } from './get_all_stats'; import { getClusterUuids } from './get_cluster_uuids'; +import { getLicenses } from './get_licenses'; export function registerMonitoringCollection() { telemetryCollectionManager.setCollection({ @@ -15,5 +16,6 @@ export function registerMonitoringCollection() { priority: 2, statsGetter: getAllStats, clusterDetailsGetter: getClusterUuids, + licenseGetter: getLicenses, }); } diff --git a/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts index e44c8f4459ba9..446db89ec09dc 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/events_viewer.spec.ts @@ -8,7 +8,6 @@ import { FIELDS_BROWSER_CHECKBOX, FIELDS_BROWSER_CONTAINER, FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, - FIELDS_BROWSER_TITLE, } from '../screens/fields_browser'; import { HEADER_SUBTITLE, @@ -61,12 +60,6 @@ describe('Events Viewer', () => { cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); }); - it('renders the fields browser with the expected title when the Events Viewer Fields Browser button is clicked', () => { - cy.get(FIELDS_BROWSER_TITLE) - .invoke('text') - .should('eq', 'Customize Columns'); - }); - it('displays the `default ECS` category (by default)', () => { cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) .invoke('text') diff --git a/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts index 095fc30356fd4..8dddd97f2d830 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/fields_browser.spec.ts @@ -15,7 +15,6 @@ import { FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, - FIELDS_BROWSER_TITLE, } from '../screens/fields_browser'; import { @@ -58,12 +57,6 @@ describe('Fields Browser', () => { clearFieldsBrowser(); }); - it('renders the fields browser with the expected title when the Fields button is clicked', () => { - cy.get(FIELDS_BROWSER_TITLE) - .invoke('text') - .should('eq', 'Customize Columns'); - }); - it('displays the `default ECS` category (by default)', () => { cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE) .invoke('text') diff --git a/x-pack/legacy/plugins/siem/cypress/support/index.js b/x-pack/legacy/plugins/siem/cypress/support/index.js index 9b86c2ffa94c6..37fa920a8bc31 100644 --- a/x-pack/legacy/plugins/siem/cypress/support/index.js +++ b/x-pack/legacy/plugins/siem/cypress/support/index.js @@ -26,5 +26,11 @@ Cypress.Cookies.defaults({ whitelist: 'sid', }); +Cypress.on('uncaught:exception', err => { + if (err.message.includes('ResizeObserver loop limit exceeded')) { + return false; + } +}); + // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap index 12b9afb661da1..c330676e9219e 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap @@ -1,5 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BarChartBaseComponent render with customized configs should 2 render BarSeries 1`] = `[Function]`; - exports[`BarChartBaseComponent render with default configs if no customized configs given should 2 render BarSeries 1`] = `[Function]`; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx index 27f0222b96b77..3c2de28ae423c 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx @@ -331,7 +331,7 @@ describe('AreaChart', () => { }); it(`should render area chart`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(1); + expect(shallowWrapper.find('AreaChartBase')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); }); @@ -344,7 +344,7 @@ describe('AreaChart', () => { }); it(`should render a chart place holder`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(0); + expect(shallowWrapper.find('AreaChartBase')).toHaveLength(0); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1); }); } diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index b66cc77e30aad..f3b2b736ed87d 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -146,8 +146,4 @@ export const AreaChartComponent: React.FC = ({ areaChar ); }; -AreaChartComponent.displayName = 'AreaChartComponent'; - export const AreaChart = React.memo(AreaChartComponent); - -AreaChart.displayName = 'AreaChart'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx index 0b6635b04d380..272c41833f368 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; -import { BarSeries, ScaleType, Axis } from '@elastic/charts'; +import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; jest.mock('../../lib/kibana'); @@ -139,7 +139,7 @@ describe('BarChartBaseComponent', () => { }); it('should render two bar series', () => { - expect(shallowWrapper.find('Chart')).toHaveLength(1); + expect(shallowWrapper.find(Chart)).toHaveLength(1); }); }); @@ -167,7 +167,6 @@ describe('BarChartBaseComponent', () => { }); it(`should ${mockBarChartData.length} render BarSeries`, () => { - expect(shallow).toMatchSnapshot(); expect(shallowWrapper.find(BarSeries)).toHaveLength(mockBarChartData.length); }); @@ -265,7 +264,7 @@ describe('BarChartBaseComponent', () => { }); it('should not render without height and width', () => { - expect(shallowWrapper.find('Chart')).toHaveLength(0); + expect(shallowWrapper.find(Chart)).toHaveLength(0); }); }); }); @@ -278,7 +277,7 @@ describe.each(chartDataSets)('BarChart with valid data [%o]', data => { }); it(`should render chart`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(1); + expect(shallowWrapper.find('BarChartBase')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); }); @@ -291,7 +290,7 @@ describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => { }); it(`should render a ChartPlaceHolder`, () => { - expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(0); + expect(shallowWrapper.find('BarChartBase')).toHaveLength(0); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx index 03b412f575646..7377bcbe7050f 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx @@ -16,7 +16,9 @@ import { TickFormatter, Position, } from '@elastic/charts'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; + import { useUiSetting } from '../../lib/kibana'; import { DEFAULT_DARK_MODE } from '../../../common/constants'; @@ -54,7 +56,7 @@ export interface ChartSeriesData { color?: string | undefined; } -export const WrappedByAutoSizer = styled.div<{ height?: string }>` +const WrappedByAutoSizerComponent = styled.div<{ height?: string }>` ${style => ` height: ${style.height != null ? style.height : defaultChartHeight}; @@ -66,7 +68,9 @@ export const WrappedByAutoSizer = styled.div<{ height?: string }>` } `; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +WrappedByAutoSizerComponent.displayName = 'WrappedByAutoSizer'; + +export const WrappedByAutoSizer = React.memo(WrappedByAutoSizerComponent); export enum SeriesType { BAR = 'bar', @@ -96,8 +100,9 @@ const theme: PartialTheme = { export const useTheme = () => { const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); const defaultTheme = isDarkMode ? DARK_THEME : LIGHT_THEME; + const themeValue = useMemo(() => mergeWithDefaultTheme(theme, defaultTheme), []); - return mergeWithDefaultTheme(theme, defaultTheme); + return themeValue; }; export const chartDefaultSettings = { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index cf958bfd75d3b..7d84403b87f8d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { createContext, useContext, useEffect } from 'react'; import { Draggable, @@ -14,6 +13,7 @@ import { } from 'react-beautiful-dnd'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { EuiPortal } from '@elastic/eui'; import { dragAndDropActions } from '../../store/drag_and_drop'; @@ -122,7 +122,7 @@ const DraggableWrapperComponent = React.memo( }, (prevProps, nextProps) => { return ( - isEqual(prevProps.dataProvider, nextProps.dataProvider) && + deepEqual(prevProps.dataProvider, nextProps.dataProvider) && prevProps.render !== nextProps.render && prevProps.truncate === nextProps.truncate ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index 2a4d08ea214bc..a913186d9ad3b 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -5,10 +5,10 @@ */ import { EuiPanel } from '@elastic/eui'; -import deepEqual from 'fast-deep-equal'; -import { getOr, isEmpty, isEqual, union } from 'lodash/fp'; +import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { BrowserFields } from '../../containers/source'; @@ -228,7 +228,7 @@ const EventsViewerComponent: React.FC = ({ export const EventsViewer = React.memo( EventsViewerComponent, (prevProps, nextProps) => - isEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && prevProps.dataProviders === nextProps.dataProviders && prevProps.deletedEventIds === nextProps.deletedEventIds && @@ -241,9 +241,9 @@ export const EventsViewer = React.memo( prevProps.itemsPerPage === nextProps.itemsPerPage && prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && prevProps.kqlMode === nextProps.kqlMode && - isEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.query, nextProps.query) && prevProps.start === nextProps.start && prevProps.sort === nextProps.sort && - isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && + deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && prevProps.utilityBar === nextProps.utilityBar ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 762ae8497dadb..9b31be40dd955 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; import { inputsActions, timelineActions } from '../../store/actions'; @@ -197,23 +197,23 @@ export const StatefulEventsViewer = connector( StatefulEventsViewerComponent, (prevProps, nextProps) => prevProps.id === nextProps.id && - isEqual(prevProps.columns, nextProps.columns) && - isEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && - isEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.filters, nextProps.filters) && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && - isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && prevProps.kqlMode === nextProps.kqlMode && - isEqual(prevProps.query, nextProps.query) && - isEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.sort, nextProps.sort) && prevProps.start === nextProps.start && - isEqual(prevProps.pageFilters, nextProps.pageFilters) && + deepEqual(prevProps.pageFilters, nextProps.pageFilters) && prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && - isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && + deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && prevProps.utilityBar === nextProps.utilityBar ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index eb41773bb21c8..22fc9f27ce26c 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -6,7 +6,7 @@ import { EuiBadge } from '@elastic/eui'; import { defaultTo, getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; @@ -58,28 +58,39 @@ export const FlyoutComponent = React.memo( timelineId, usersViewing, width, - }) => ( - <> - - showTimeline({ id: timelineId, show: false })} + }) => { + const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ + showTimeline, + timelineId, + ]); + const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ + showTimeline, + timelineId, + ]); + + return ( + <> + + + {children} + + + - {children} - - - showTimeline({ id: timelineId, show: true })} - /> - - ) + onOpen={handleOpen} + /> + + ); + } ); FlyoutComponent.displayName = 'FlyoutComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index 405a8f060948e..d6f8143745356 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; -import { getOr } from 'lodash/fp'; +import { getOr, omit } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -162,7 +162,11 @@ const makeMapStateToProps = () => { const getGlobalQuery = inputsSelectors.globalQueryByIdSelector(); const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); const mapStateToProps = (state: State, { inputId = 'global', queryId }: OwnProps) => { - return inputId === 'global' ? getGlobalQuery(state, queryId) : getTimelineQuery(state, queryId); + const props = + inputId === 'global' ? getGlobalQuery(state, queryId) : getTimelineQuery(state, queryId); + // refetch caused unnecessary component rerender and it was even not used + const propsWithoutRefetch = omit('refetch', props); + return propsWithoutRefetch; }; return mapStateToProps; }; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index e40cc887ab5ff..8a754cb47475f 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import isEqual from 'lodash/fp/isEqual'; -import deepEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; +import deepEqual from 'fast-deep-equal'; import { useKibana } from '../../lib/kibana'; import { RouteSpyState } from '../../utils/route/types'; @@ -81,8 +80,8 @@ export const SiemNavigationRedux = compose< (prevProps, nextProps) => prevProps.pathName === nextProps.pathName && prevProps.search === nextProps.search && - isEqual(prevProps.navTabs, nextProps.navTabs) && - isEqual(prevProps.urlState, nextProps.urlState) && + deepEqual(prevProps.navTabs, nextProps.navTabs) && + deepEqual(prevProps.urlState, nextProps.urlState) && deepEqual(prevProps.state, nextProps.state) ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx index 853ba7ae23414..678faff7654db 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx @@ -7,7 +7,7 @@ /* eslint-disable react/display-name */ import { has } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { hostsActions } from '../../../../store/hosts'; @@ -100,10 +100,12 @@ const AuthenticationTableComponent = React.memo( [type, updateTableActivePage] ); + const columns = useMemo(() => getAuthenticationColumnsCurated(type), [type]); + return ( ( [type, updateTableActivePage] ); + const columns = useMemo(() => getUncommonColumnsCurated(type), [type]); + return ( ]; -export const getNetworkDnsColumns = (type: networkModel.NetworkType): NetworkDnsColumns => [ +export const getNetworkDnsColumns = (): NetworkDnsColumns => [ { field: `node.${NetworkDnsFields.dnsName}`, name: i18n.REGISTERED_DOMAIN, diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx index f3fe98936a55d..c1dd96c5c96f9 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { networkActions } from '../../../../store/actions'; import { @@ -93,7 +93,7 @@ export const NetworkDnsTableComponent = React.memo( field: criteria.sort.field.split('.')[1] as NetworkDnsFields, direction: criteria.sort.direction as Direction, }; - if (!isEqual(newDnsSortField, sort)) { + if (!deepEqual(newDnsSortField, sort)) { updateNetworkTable({ networkType: type, tableType, @@ -115,10 +115,12 @@ export const NetworkDnsTableComponent = React.memo( [type, updateNetworkTable, isPtrIncluded] ); + const columns = useMemo(() => getNetworkDnsColumns(), []); + return ( = ({ const sorting = { field: `node.${NetworkHttpFields.requestCount}`, direction: sort.direction }; + const columns = useMemo(() => getNetworkHttpColumns(tableType), [tableType]); + return ( = ({ field: field as NetworkTopTablesFields, direction: newSortDirection as Direction, }; - if (!isEqual(newTopNFlowSort, sort)) { + if (!deepEqual(newTopNFlowSort, sort)) { updateNetworkTable({ networkType: type, tableType, diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx index 77abae68b76bf..d1512699cc709 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { networkActions } from '../../../../store/network'; import { TlsEdges, TlsSortField, TlsFields, Direction } from '../../../../graphql/types'; @@ -91,7 +91,7 @@ const TlsTableComponent = React.memo( field: getSortFromString(splitField[splitField.length - 1]), direction: criteria.sort.direction as Direction, }; - if (!isEqual(newTlsSort, sort)) { + if (!deepEqual(newTlsSort, sort)) { updateNetworkTable({ networkType: type, tableType, @@ -103,10 +103,12 @@ const TlsTableComponent = React.memo( [sort, type, tableType, updateNetworkTable] ); + const columns = useMemo(() => getTlsColumns(tlsTableId), [tlsTableId]); + return ( ( field: getSortFromString(splitField[splitField.length - 1]), direction: criteria.sort.direction as Direction, }; - if (!isEqual(newUsersSort, sort)) { + if (!deepEqual(newUsersSort, sort)) { updateNetworkTable({ networkType: type, tableType, @@ -109,10 +109,15 @@ const UsersTableComponent = React.memo( [sort, type, updateNetworkTable] ); + const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [ + flowTarget, + usersTableId, + ]); + return ( = ({ data, loading ); }; -OverviewHostStatsComponent.displayName = 'OverviewHostStatsComponent'; - export const OverviewHostStats = React.memo(OverviewHostStatsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx index 260b1d6895140..ca947c29bc382 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx @@ -130,7 +130,7 @@ const AccordionContent = styled.div` margin-top: 8px; `; -export const OverviewNetworkStats = React.memo(({ data, loading }) => { +const OverviewNetworkStatsComponent: React.FC = ({ data, loading }) => { const allNetworkStats = getOverviewNetworkStats(data); const allNetworkStatsCount = allNetworkStats.reduce((total, stat) => total + stat.count, 0); @@ -190,6 +190,6 @@ export const OverviewNetworkStats = React.memo(({ data, lo })} ); -}); +}; -OverviewNetworkStats.displayName = 'OverviewNetworkStats'; +export const OverviewNetworkStats = React.memo(OverviewNetworkStatsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx index 03a8143c89517..557d389aefee9 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; +import deepEqual from 'fast-deep-equal'; import { Filter, @@ -64,7 +64,7 @@ export const QueryBar = memo( const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !isEqual(payload.query, filterQuery)) { + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { onSubmitQuery(payload.query); } }, @@ -73,7 +73,7 @@ export const QueryBar = memo( const onQueryChange = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !isEqual(payload.query, draftQuery)) { + if (payload.query != null && !deepEqual(payload.query, draftQuery)) { setDraftQuery(payload.query); onChangedQuery(payload.query); } diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index cb5729ad8e26e..2513004af84dd 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, isEqual, set } from 'lodash/fp'; +import { getOr, set } from 'lodash/fp'; import React, { memo, useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { FilterManager, IIndexPattern, TimeRange, Query, Filter } from 'src/plugins/data/public'; import { SavedQuery } from 'src/legacy/core_plugins/data/public'; @@ -60,7 +61,6 @@ const SearchBarComponent = memo( setSavedQuery, setSearchBarFilter, start, - timelineId, toStr, updateSearch, dataTestSubj, @@ -108,7 +108,7 @@ const SearchBarComponent = memo( updateSearchBar.start = payload.dateRange.from; } - if (payload.query != null && !isEqual(payload.query, filterQuery)) { + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { isStateUpdated = true; updateSearchBar = set('query', payload.query, updateSearchBar); } diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx index 33159387214e4..b8192cce11e5a 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx @@ -5,8 +5,9 @@ */ import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty, isEqual, uniqWith } from 'lodash/fp'; +import { isEmpty, uniqWith } from 'lodash/fp'; import React from 'react'; +import deepEqual from 'fast-deep-equal'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME, Port } from '../port'; @@ -115,7 +116,7 @@ const IpAdressesWithPorts = React.memo<{ return ( - {uniqWith(isEqual, ipPortPairs).map( + {uniqWith(deepEqual, ipPortPairs).map( ipPortPair => ipPortPair.ip != null && ( diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap index 69596ba8f3325..ef077ece19f92 100644 --- a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap @@ -94,7 +94,6 @@ exports[`Stat Items Component disable charts it renders the default widget 1`] = isInspected={false} loading={false} queryId="statItems" - refetch={null} selectedInspectIndex={0} setIsInspected={[Function]} title="KPI HOSTS" @@ -328,7 +327,6 @@ exports[`Stat Items Component disable charts it renders the default widget 2`] = isInspected={false} loading={false} queryId="statItems" - refetch={null} selectedInspectIndex={0} setIsInspected={[Function]} title="KPI HOSTS" @@ -632,7 +630,6 @@ exports[`Stat Items Component rendering kpis with charts it renders the default isInspected={false} loading={false} queryId="statItems" - refetch={null} selectedInspectIndex={0} setIsInspected={[Function]} title="KPI UNIQUE_PRIVATE_IPS" diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 3fcd258b79147..372930ee3167d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -8,7 +8,7 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` justifyContent="flexStart" > - = ({ browserFields, id, indexPattern, @@ -60,7 +60,7 @@ export const TimelineHeaderComponent = ({ onToggleDataProviderExcluded, show, showCallOutUnauthorizedMsg, -}: Props) => ( +}) => ( {showCallOutUnauthorizedMsg && ( ); -TimelineHeaderComponent.displayName = 'TimelineHeaderComponent'; - export const TimelineHeader = React.memo(TimelineHeaderComponent); - -TimelineHeader.displayName = 'TimelineHeader'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index d782d0366f041..0ce6bc16f1325 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { WithSource } from '../../containers/source'; import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; @@ -215,11 +215,11 @@ const StatefulTimelineComponent = React.memo( prevProps.show === nextProps.show && prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && prevProps.start === nextProps.start && - isEqual(prevProps.columns, nextProps.columns) && - isEqual(prevProps.dataProviders, nextProps.dataProviders) && - isEqual(prevProps.filters, nextProps.filters) && - isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - isEqual(prevProps.sort, nextProps.sort) + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + deepEqual(prevProps.sort, nextProps.sort) ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx index 96b8df6d8ada7..7f662cdb2f1b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual, isEmpty } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import React, { memo, useCallback, useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern, @@ -127,7 +128,7 @@ export const QueryBarTimeline = memo( const filterWithoutDropArea = filterManager .getFilters() .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - if (!isEqual(filters, filterWithoutDropArea)) { + if (!deepEqual(filters, filterWithoutDropArea)) { filterManager.setFilters(filters); } }, [filters]); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index d1904fd5d9aac..87061bdbb5d02 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, isEqual } from 'lodash/fp'; +import { getOr } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; +import deepEqual from 'fast-deep-equal'; import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; @@ -152,15 +153,15 @@ const StatefulSearchOrFilterComponent = React.memo( prevProps.isRefreshPaused === nextProps.isRefreshPaused && prevProps.refreshInterval === nextProps.refreshInterval && prevProps.timelineId === nextProps.timelineId && - isEqual(prevProps.browserFields, nextProps.browserFields) && - isEqual(prevProps.dataProviders, nextProps.dataProviders) && - isEqual(prevProps.filters, nextProps.filters) && - isEqual(prevProps.filterQuery, nextProps.filterQuery) && - isEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && - isEqual(prevProps.indexPattern, nextProps.indexPattern) && - isEqual(prevProps.kqlMode, nextProps.kqlMode) && - isEqual(prevProps.savedQueryId, nextProps.savedQueryId) && - isEqual(prevProps.timelineId, nextProps.timelineId) + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.filterQuery, nextProps.filterQuery) && + deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.kqlMode, nextProps.kqlMode) && + deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && + deepEqual(prevProps.timelineId, nextProps.timelineId) ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index c9ff0296a40e2..58bbbef328ddf 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -229,8 +229,4 @@ export const TimelineComponent: React.FC = ({ ); }; -TimelineComponent.displayName = 'TimelineComponent'; - export const Timeline = React.memo(TimelineComponent); - -Timeline.displayName = 'Timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx index e656ec3496d8d..294e41a1faa7b 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import React from 'react'; import { compose, Dispatch } from 'redux'; import { connect } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { timelineActions } from '../../store/actions'; import { RouteSpyState } from '../../utils/route/types'; @@ -39,7 +39,7 @@ export const UrlStateRedux = compose - prevProps.pathName === nextProps.pathName && isEqual(prevProps.urlState, nextProps.urlState) + prevProps.pathName === nextProps.pathName && deepEqual(prevProps.urlState, nextProps.urlState) ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx index deaf9bbf5011d..a7704e0e86970 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual, difference, isEmpty } from 'lodash/fp'; +import { difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; import { useKibana } from '../../lib/kibana'; import { useApolloClient } from '../../utils/apollo_context'; @@ -77,7 +78,7 @@ export const useUrlStateHooks = ({ const updatedUrlStateString = getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ?? newUrlStateString; - if (isInitializing || !isEqual(updatedUrlStateString, newUrlStateString)) { + if (isInitializing || !deepEqual(updatedUrlStateString, newUrlStateString)) { urlStateToUpdate = [ ...urlStateToUpdate, { @@ -157,7 +158,7 @@ export const useUrlStateHooks = ({ if (isInitializing && pageName != null && pageName !== '') { handleInitialize(type); setIsInitializing(false); - } else if (!isEqual(urlState, prevProps.urlState) && !isInitializing) { + } else if (!deepEqual(urlState, prevProps.urlState) && !isInitializing) { let mySearch = search; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { if ( diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index b7ad41b8ba1bb..06c4d1054bca4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, isEqual, get } from 'lodash/fp'; +import { isEmpty, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { @@ -41,7 +42,7 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => const [, dispatchToaster] = useStateToaster(); useEffect(() => { - if (!isEqual(defaultIndices, indices)) { + if (!deepEqual(defaultIndices, indices)) { setIndices(defaultIndices); } }, [defaultIndices, indices]); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx index b369d3a50730d..242d715e20f77 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx @@ -5,9 +5,8 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { useRules, ReturnRules } from './use_rules'; +import { useRules, UseRules, ReturnRules } from './use_rules'; import * as api from './api'; -import { PaginationOptions, FilterOptions } from '.'; jest.mock('./api'); @@ -17,55 +16,40 @@ describe('useRules', () => { }); test('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(props => - useRules( - { + const { result, waitForNextUpdate } = renderHook(props => + useRules({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: '', sortField: 'created_at', sortOrder: 'desc', - } - ) + }, + }) ); await waitForNextUpdate(); - expect(result.current).toEqual([ - true, - { - data: [], - page: 1, - perPage: 20, - total: 0, - }, - null, - ]); + expect(result.current).toEqual([true, null, result.current[2]]); }); }); test('fetch rules', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(() => - useRules( - { + const { result, waitForNextUpdate } = renderHook(() => + useRules({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: '', sortField: 'created_at', sortOrder: 'desc', - } - ) + }, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -148,22 +132,19 @@ describe('useRules', () => { test('re-fetch rules', async () => { const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); await act(async () => { - const { result, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(id => - useRules( - { + const { result, waitForNextUpdate } = renderHook(id => + useRules({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: '', sortField: 'created_at', sortOrder: 'desc', - } - ) + }, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -178,37 +159,37 @@ describe('useRules', () => { test('fetch rules if props changes', async () => { const spyOnfetchRules = jest.spyOn(api, 'fetchRules'); await act(async () => { - const { rerender, waitForNextUpdate } = renderHook< - [PaginationOptions, FilterOptions], - ReturnRules - >(args => useRules(args[0], args[1]), { - initialProps: [ - { - page: 1, - perPage: 10, - total: 100, - }, - { - filter: '', - sortField: 'created_at', - sortOrder: 'desc', + const { rerender, waitForNextUpdate } = renderHook( + args => useRules(args), + { + initialProps: { + pagination: { + page: 1, + perPage: 10, + total: 100, + }, + filterOptions: { + filter: '', + sortField: 'created_at', + sortOrder: 'desc', + }, }, - ], - }); + } + ); await waitForNextUpdate(); await waitForNextUpdate(); - rerender([ - { + rerender({ + pagination: { page: 1, perPage: 10, total: 100, }, - { + filterOptions: { filter: 'hello world', sortField: 'created_at', sortOrder: 'desc', }, - ]); + }); await waitForNextUpdate(); expect(spyOnfetchRules).toHaveBeenCalledTimes(2); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index 301a68dc6f445..d05d59d15802d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -4,16 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; -import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types'; +import { FetchRulesResponse, FilterOptions, PaginationOptions, Rule } from './types'; import { useStateToaster } from '../../../components/toasters'; import { fetchRules } from './api'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -type Func = () => void; -export type ReturnRules = [boolean, FetchRulesResponse, Func | null]; +export type ReturnRules = [ + boolean, + FetchRulesResponse | null, + (refreshPrePackagedRule?: boolean) => void +]; + +export interface UseRules { + pagination: PaginationOptions; + filterOptions: FilterOptions; + refetchPrePackagedRulesStatus?: () => void; + dispatchRulesInReducer?: (rules: Rule[]) => void; +} /** * Hook for using the list of Rules from the Detection Engine API @@ -21,17 +32,14 @@ export type ReturnRules = [boolean, FetchRulesResponse, Func | null]; * @param pagination desired pagination options (e.g. page/perPage) * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) */ -export const useRules = ( - pagination: PaginationOptions, - filterOptions: FilterOptions -): ReturnRules => { - const [rules, setRules] = useState({ - page: 1, - perPage: 20, - total: 0, - data: [], - }); - const reFetchRules = useRef(null); +export const useRules = ({ + pagination, + filterOptions, + refetchPrePackagedRulesStatus, + dispatchRulesInReducer, +}: UseRules): ReturnRules => { + const [rules, setRules] = useState(null); + const reFetchRules = useRef<(refreshPrePackagedRule?: boolean) => void>(noop); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -50,10 +58,16 @@ export const useRules = ( if (isSubscribed) { setRules(fetchRulesResult); + if (dispatchRulesInReducer != null) { + dispatchRulesInReducer(fetchRulesResult.data); + } } } catch (error) { if (isSubscribed) { errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + if (dispatchRulesInReducer != null) { + dispatchRulesInReducer([]); + } } } if (isSubscribed) { @@ -62,7 +76,12 @@ export const useRules = ( } fetchData(); - reFetchRules.current = fetchData.bind(null, true); + reFetchRules.current = (refreshPrePackagedRule: boolean = false) => { + fetchData(true); + if (refreshPrePackagedRule && refetchPrePackagedRulesStatus != null) { + refetchPrePackagedRulesStatus(); + } + }; return () => { isSubscribed = false; abortCtrl.abort(); @@ -76,6 +95,7 @@ export const useRules = ( filterOptions.tags?.sort().join(), filterOptions.showCustomRules, filterOptions.showElasticRules, + refetchPrePackagedRulesStatus, ]); return [loading, rules, reFetchRules.current]; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx index 4a796efa5b0cb..68f54b35754f6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx @@ -14,7 +14,7 @@ describe('useTags', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useTags()); await waitForNextUpdate(); - expect(result.current).toEqual([true, []]); + expect(result.current).toEqual([true, [], result.current[2]]); }); }); @@ -23,7 +23,11 @@ describe('useTags', () => { const { result, waitForNextUpdate } = renderHook(() => useTags()); await waitForNextUpdate(); await waitForNextUpdate(); - expect(result.current).toEqual([false, ['elastic', 'love', 'quality', 'code']]); + expect(result.current).toEqual([ + false, + ['elastic', 'love', 'quality', 'code'], + result.current[2], + ]); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx index 196d4b1420561..5985200fa16ec 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { noop } from 'lodash/fp'; +import { useEffect, useState, useRef } from 'react'; import { useStateToaster } from '../../../components/toasters'; import { fetchTags } from './api'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import * as i18n from './translations'; -export type ReturnTags = [boolean, string[]]; +export type ReturnTags = [boolean, string[], () => void]; /** * Hook for using the list of Tags from the Detection Engine API @@ -20,6 +21,7 @@ export const useTags = (): ReturnTags => { const [tags, setTags] = useState([]); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); + const reFetchTags = useRef<() => void>(noop); useEffect(() => { let isSubscribed = true; @@ -46,6 +48,7 @@ export const useTags = (): ReturnTags => { } fetchData(); + reFetchTags.current = fetchData; return () => { isSubscribed = false; @@ -53,5 +56,5 @@ export const useTags = (): ReturnTags => { }; }, []); - return [loading, tags]; + return [loading, tags, reFetchTags.current]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx index caf597d02c835..4632e9aee3fdd 100644 --- a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -41,6 +41,17 @@ export const GlobalTimeComponent: React.FC = ({ }) => { const [isInitializing, setIsInitializing] = useState(true); + const setQuery = useCallback( + ({ id, inspect, loading, refetch }: SetQuery) => + setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), + [setGlobalQuery] + ); + + const deleteQuery = useCallback( + ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), + [deleteOneQuery] + ); + useEffect(() => { if (isInitializing) { setIsInitializing(false); @@ -56,9 +67,8 @@ export const GlobalTimeComponent: React.FC = ({ isInitializing, from, to, - setQuery: ({ id, inspect, loading, refetch }: SetQuery) => - setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), - deleteQuery: ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), + setQuery, + deleteQuery, })} ); diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx index 4d6ab757fdea7..db618f216d83e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx @@ -5,10 +5,10 @@ */ import { ApolloQueryResult, NetworkStatus } from 'apollo-client'; -import { isEqual } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; +import deepEqual from 'fast-deep-equal'; import { ESQuery } from '../../common/typed_json'; import { inputsModel } from '../store/model'; @@ -85,7 +85,7 @@ export class QueryTemplatePaginated< public isItAValidLoading(loading: boolean, variables: TVariables, networkStatus: NetworkStatus) { if ( !this.myLoading && - (!isEqual(variables, this.queryVariables) || networkStatus === NetworkStatus.refetch) && + (!deepEqual(variables, this.queryVariables) || networkStatus === NetworkStatus.refetch) && loading ) { this.myLoading = true; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index 0336e4a9a977b..e454421ca955d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -10,6 +10,7 @@ import { Query } from 'react-apollo'; import React, { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; + import { useUiSetting$ } from '../../lib/kibana'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx index 92f6740e4d767..2d9b1ee844b4b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx @@ -12,9 +12,11 @@ import { HistogramBarSeries, Position, Settings, + ChartSizeArray, } from '@elastic/charts'; -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiProgress } from '@elastic/eui'; + import { useTheme } from '../../../../components/charts/common'; import { histogramDateTimeFormatter } from '../../../../components/utils'; import { HistogramData } from './types'; @@ -43,6 +45,14 @@ export const SignalsHistogram = React.memo( }) => { const theme = useTheme(); + const chartSize: ChartSizeArray = useMemo(() => ['100%', chartHeight], [chartHeight]); + const xAxisId = useMemo(() => getAxisId('signalsHistogramAxisX'), []); + const yAxisId = useMemo(() => getAxisId('signalsHistogramAxisY'), []); + const id = useMemo(() => getSpecId('signalsHistogram'), []); + const yAccessors = useMemo(() => ['y'], []); + const splitSeriesAccessors = useMemo(() => ['g'], []); + const tickFormat = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]); + return ( <> {loading && ( @@ -54,7 +64,7 @@ export const SignalsHistogram = React.memo( /> )} - + ( theme={theme} /> - + - + @@ -84,4 +90,5 @@ export const SignalsHistogram = React.memo( ); } ); + SignalsHistogram.displayName = 'SignalsHistogram'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index 980575f1470a5..e2287e5eeeb3f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -5,7 +5,6 @@ */ import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; -import { TableData } from '../../types'; export const mockRule = (id: string): Rule => ({ created_at: '2020-01-10T21:11:45.839Z', @@ -50,103 +49,3 @@ export const mockRules: Rule[] = [ mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), ]; -export const mockTableData: TableData[] = [ - { - activate: true, - id: 'abe6c564-050d-45a5-aaf0-386c37dd1f61', - immutable: false, - isLoading: false, - risk_score: 21, - rule: { - href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61', - name: 'Home Grown!', - }, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - severity: 'low', - sourceRule: { - created_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: [], - filters: [], - from: 'now-300s', - id: 'abe6c564-050d-45a5-aaf0-386c37dd1f61', - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { from: '0m' }, - name: 'Home Grown!', - output_index: '.siem-signals-default', - query: '', - references: [], - risk_score: 21, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - saved_id: "Garrett's IP", - severity: 'low', - tags: [], - threat: [], - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Untitled timeline', - to: 'now', - type: 'saved_query', - updated_at: '2020-01-10T21:11:45.839Z', - updated_by: 'elastic', - version: 1, - }, - status: null, - statusDate: null, - tags: [], - }, - { - activate: true, - id: '63f06f34-c181-4b2d-af35-f2ace572a1ee', - immutable: false, - isLoading: false, - risk_score: 21, - rule: { - href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee', - name: 'Home Grown!', - }, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - severity: 'low', - sourceRule: { - created_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: [], - filters: [], - from: 'now-300s', - id: '63f06f34-c181-4b2d-af35-f2ace572a1ee', - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { from: '0m' }, - name: 'Home Grown!', - output_index: '.siem-signals-default', - query: '', - references: [], - risk_score: 21, - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - saved_id: "Garrett's IP", - severity: 'low', - tags: [], - threat: [], - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Untitled timeline', - to: 'now', - type: 'saved_query', - updated_at: '2020-01-10T21:11:45.839Z', - updated_by: 'elastic', - version: 1, - }, - status: null, - statusDate: null, - tags: [], - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index 6212c2067384d..a17fd34d1c344 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -32,53 +32,58 @@ export const editRuleAction = (rule: Rule, history: H.History) => { export const duplicateRulesAction = async ( rules: Rule[], + ruleIds: string[], dispatch: React.Dispatch, dispatchToaster: Dispatch ) => { try { - const ruleIds = rules.map(r => r.id); - dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true }); - const duplicatedRules = await duplicateRules({ rules }); - dispatch({ type: 'refresh' }); - displaySuccessToast( - i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length), - dispatchToaster - ); + dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' }); + const response = await duplicateRules({ rules }); + const { errors } = bucketRulesResponse(response); + if (errors.length > 0) { + displayErrorToast( + i18n.DUPLICATE_RULE_ERROR, + errors.map(e => e.error.message), + dispatchToaster + ); + } else { + displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster); + } + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); } catch (e) { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster); } }; -export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch) => { - dispatch({ type: 'setExportPayload', exportPayload: rules }); +export const exportRulesAction = (exportRuleId: string[], dispatch: React.Dispatch) => { + dispatch({ type: 'exportRuleIds', ids: exportRuleId }); }; export const deleteRulesAction = async ( - ids: string[], + ruleIds: string[], dispatch: React.Dispatch, dispatchToaster: Dispatch, onRuleDeleted?: () => void ) => { try { - dispatch({ type: 'loading', isLoading: true }); - - const response = await deleteRules({ ids }); + dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'delete' }); + const response = await deleteRules({ ids: ruleIds }); const { errors } = bucketRulesResponse(response); - - dispatch({ type: 'refresh' }); + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); if (errors.length > 0) { displayErrorToast( - i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length), + i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), errors.map(e => e.error.message), dispatchToaster ); - } else { - // FP: See https://github.com/typescript-eslint/typescript-eslint/issues/1138#issuecomment-566929566 - onRuleDeleted?.(); // eslint-disable-line no-unused-expressions + } else if (onRuleDeleted) { + onRuleDeleted(); } } catch (e) { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); displayErrorToast( - i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ids.length), + i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), [e.message], dispatchToaster ); @@ -96,7 +101,7 @@ export const enableRulesAction = async ( : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(ids.length); try { - dispatch({ type: 'updateLoading', ids, isLoading: true }); + dispatch({ type: 'loadingRuleIds', ids, actionType: enabled ? 'enable' : 'disable' }); const response = await enableRules({ ids, enabled }); const { rules, errors } = bucketRulesResponse(response); @@ -125,6 +130,6 @@ export const enableRulesAction = async ( } } catch (e) { displayErrorToast(errorTitle, [e.message], dispatchToaster); - dispatch({ type: 'updateLoading', ids, isLoading: false }); + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); } }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx index 8a10d4f7100b9..a0942d7f6534a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx @@ -6,9 +6,7 @@ import { EuiContextMenuItem } from '@elastic/eui'; import React, { Dispatch } from 'react'; -import * as H from 'history'; import * as i18n from '../translations'; -import { TableData } from '../types'; import { Action } from './reducer'; import { deleteRulesAction, @@ -17,18 +15,37 @@ import { exportRulesAction, } from './actions'; import { ActionToaster } from '../../../../components/toasters'; +import { Rule } from '../../../../containers/detection_engine/rules'; -export const getBatchItems = ( - selectedState: TableData[], - dispatch: Dispatch, - dispatchToaster: Dispatch, - history: H.History, - closePopover: () => void -) => { - const containsEnabled = selectedState.some(v => v.activate); - const containsDisabled = selectedState.some(v => !v.activate); - const containsLoading = selectedState.some(v => v.isLoading); - const containsImmutable = selectedState.some(v => v.immutable); +interface GetBatchItems { + closePopover: () => void; + dispatch: Dispatch; + dispatchToaster: Dispatch; + loadingRuleIds: string[]; + reFetchRules: (refreshPrePackagedRule?: boolean) => void; + rules: Rule[]; + selectedRuleIds: string[]; +} + +export const getBatchItems = ({ + closePopover, + dispatch, + dispatchToaster, + loadingRuleIds, + reFetchRules, + rules, + selectedRuleIds, +}: GetBatchItems) => { + const containsEnabled = selectedRuleIds.some( + id => rules.find(r => r.id === id)?.enabled ?? false + ); + const containsDisabled = selectedRuleIds.some( + id => !rules.find(r => r.id === id)?.enabled ?? false + ); + const containsLoading = selectedRuleIds.some(id => loadingRuleIds.includes(id)); + const containsImmutable = selectedRuleIds.some( + id => rules.find(r => r.id === id)?.immutable ?? false + ); return [ { closePopover(); - const deactivatedIds = selectedState.filter(s => !s.activate).map(s => s.id); + const deactivatedIds = selectedRuleIds.filter( + id => !rules.find(r => r.id === id)?.enabled ?? false + ); await enableRulesAction(deactivatedIds, true, dispatch, dispatchToaster); }} > @@ -49,7 +68,9 @@ export const getBatchItems = ( disabled={containsLoading || !containsEnabled} onClick={async () => { closePopover(); - const activatedIds = selectedState.filter(s => s.activate).map(s => s.id); + const activatedIds = selectedRuleIds.filter( + id => rules.find(r => r.id === id)?.enabled ?? false + ); await enableRulesAction(activatedIds, false, dispatch, dispatchToaster); }} > @@ -58,11 +79,11 @@ export const getBatchItems = ( { + disabled={containsImmutable || containsLoading || selectedRuleIds.length === 0} + onClick={() => { closePopover(); - await exportRulesAction( - selectedState.map(s => s.sourceRule), + exportRulesAction( + rules.filter(r => selectedRuleIds.includes(r.id)).map(r => r.rule_id), dispatch ); }} @@ -72,14 +93,16 @@ export const getBatchItems = ( { closePopover(); await duplicateRulesAction( - selectedState.map(s => s.sourceRule), + rules.filter(r => selectedRuleIds.includes(r.id)), + selectedRuleIds, dispatch, dispatchToaster ); + reFetchRules(true); }} > {i18n.BATCH_ACTION_DUPLICATE_SELECTED} @@ -88,14 +111,11 @@ export const getBatchItems = ( key={i18n.BATCH_ACTION_DELETE_SELECTED} icon="trash" title={containsImmutable ? i18n.BATCH_ACTION_DELETE_SELECTED_IMMUTABLE : undefined} - disabled={containsLoading || selectedState.length === 0} + disabled={containsLoading || selectedRuleIds.length === 0} onClick={async () => { closePopover(); - await deleteRulesAction( - selectedState.map(({ sourceRule: { id } }) => id), - dispatch, - dispatchToaster - ); + await deleteRulesAction(selectedRuleIds, dispatch, dispatchToaster); + reFetchRules(true); }} > {i18n.BATCH_ACTION_DELETE_SELECTED} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index d648854368c28..ff104f09d68ef 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -15,72 +15,92 @@ import { } from '@elastic/eui'; import * as H from 'history'; import React, { Dispatch } from 'react'; + +import { Rule } from '../../../../containers/detection_engine/rules'; import { getEmptyTagValue } from '../../../../components/empty_value'; +import { FormattedDate } from '../../../../components/formatted_date'; +import { getRuleDetailsUrl } from '../../../../components/link_to/redirect_to_detection_engine'; +import { ActionToaster } from '../../../../components/toasters'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import { getStatusColor } from '../components/rule_status/helpers'; +import { RuleSwitch } from '../components/rule_switch'; +import { SeverityBadge } from '../components/severity_badge'; +import * as i18n from '../translations'; import { deleteRulesAction, duplicateRulesAction, editRuleAction, exportRulesAction, } from './actions'; - import { Action } from './reducer'; -import { TableData } from '../types'; -import * as i18n from '../translations'; -import { FormattedDate } from '../../../../components/formatted_date'; -import { RuleSwitch } from '../components/rule_switch'; -import { SeverityBadge } from '../components/severity_badge'; -import { ActionToaster } from '../../../../components/toasters'; -import { getStatusColor } from '../components/rule_status/helpers'; -import { TruncatableText } from '../../../../components/truncatable_text'; const getActions = ( dispatch: React.Dispatch, dispatchToaster: Dispatch, - history: H.History + history: H.History, + reFetchRules: (refreshPrePackagedRule?: boolean) => void ) => [ { description: i18n.EDIT_RULE_SETTINGS, icon: 'visControls', name: i18n.EDIT_RULE_SETTINGS, - onClick: (rowItem: TableData) => editRuleAction(rowItem.sourceRule, history), - enabled: (rowItem: TableData) => !rowItem.sourceRule.immutable, + onClick: (rowItem: Rule) => editRuleAction(rowItem, history), + enabled: (rowItem: Rule) => !rowItem.immutable, }, { description: i18n.DUPLICATE_RULE, icon: 'copy', name: i18n.DUPLICATE_RULE, - onClick: (rowItem: TableData) => - duplicateRulesAction([rowItem.sourceRule], dispatch, dispatchToaster), + onClick: (rowItem: Rule) => { + duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster); + reFetchRules(true); + }, }, { description: i18n.EXPORT_RULE, icon: 'exportAction', name: i18n.EXPORT_RULE, - onClick: (rowItem: TableData) => exportRulesAction([rowItem.sourceRule], dispatch), - enabled: (rowItem: TableData) => !rowItem.immutable, + onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch), + enabled: (rowItem: Rule) => !rowItem.immutable, }, { description: i18n.DELETE_RULE, icon: 'trash', name: i18n.DELETE_RULE, - onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster), + onClick: (rowItem: Rule) => { + deleteRulesAction([rowItem.id], dispatch, dispatchToaster); + reFetchRules(true); + }, }, ]; -type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; +type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; + +interface GetColumns { + dispatch: React.Dispatch; + dispatchToaster: Dispatch; + history: H.History; + hasNoPermissions: boolean; + loadingRuleIds: string[]; + reFetchRules: (refreshPrePackagedRule?: boolean) => void; +} // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? -export const getColumns = ( - dispatch: React.Dispatch, - dispatchToaster: Dispatch, - history: H.History, - hasNoPermissions: boolean -): RulesColumns[] => { +export const getColumns = ({ + dispatch, + dispatchToaster, + history, + hasNoPermissions, + loadingRuleIds, + reFetchRules, +}: GetColumns): RulesColumns[] => { const cols: RulesColumns[] = [ { - field: 'rule', + field: 'name', name: i18n.COLUMN_RULE, - render: (value: TableData['rule']) => {value.name}, + render: (value: Rule['name'], item: Rule) => ( + {value} + ), truncateText: true, width: '24%', }, @@ -93,14 +113,14 @@ export const getColumns = ( { field: 'severity', name: i18n.COLUMN_SEVERITY, - render: (value: TableData['severity']) => , + render: (value: Rule['severity']) => , truncateText: true, width: '16%', }, { - field: 'statusDate', + field: 'status_date', name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: TableData['statusDate']) => { + render: (value: Rule['status_date']) => { return value == null ? ( getEmptyTagValue() ) : ( @@ -114,7 +134,7 @@ export const getColumns = ( { field: 'status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: TableData['status']) => { + render: (value: Rule['status']) => { return ( <> @@ -129,7 +149,7 @@ export const getColumns = ( { field: 'tags', name: i18n.COLUMN_TAGS, - render: (value: TableData['tags']) => ( + render: (value: Rule['tags']) => ( {value.map((tag, i) => ( @@ -145,13 +165,13 @@ export const getColumns = ( align: 'center', field: 'activate', name: i18n.COLUMN_ACTIVATE, - render: (value: TableData['activate'], item: TableData) => ( + render: (value: Rule['enabled'], item: Rule) => ( ), sortable: true, @@ -160,9 +180,9 @@ export const getColumns = ( ]; const actions: RulesColumns[] = [ { - actions: getActions(dispatch, dispatchToaster, history), + actions: getActions(dispatch, dispatchToaster, history, reFetchRules), width: '40px', - } as EuiTableActionsColumnType, + } as EuiTableActionsColumnType, ]; return hasNoPermissions ? cols : [...cols, ...actions]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx index e925161444e42..c60933733587d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { bucketRulesResponse, formatRules } from './helpers'; -import { mockRule, mockRuleError, mockRules, mockTableData } from './__mocks__/mock'; +import { bucketRulesResponse } from './helpers'; +import { mockRule, mockRuleError } from './__mocks__/mock'; import uuid from 'uuid'; import { Rule, RuleError } from '../../../../containers/detection_engine/rules'; @@ -15,20 +15,6 @@ describe('AllRulesTable Helpers', () => { const mockRuleError1: Readonly = mockRuleError(uuid.v4()); const mockRuleError2: Readonly = mockRuleError(uuid.v4()); - describe('formatRules', () => { - test('formats rules with no selection', () => { - const formattedRules = formatRules(mockRules); - expect(formattedRules).toEqual(mockTableData); - }); - - test('formats rules with selection', () => { - const mockTableDataWithSelected = [...mockTableData]; - mockTableDataWithSelected[0].isLoading = true; - const formattedRules = formatRules(mockRules, [mockRules[0].id]); - expect(formattedRules).toEqual(mockTableDataWithSelected); - }); - }); - describe('bucketRulesResponse', () => { test('buckets empty response', () => { const bucketedResponse = bucketRulesResponse([]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index 9a523536694d9..5ce26144a4d9c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -9,32 +9,6 @@ import { RuleError, RuleResponseBuckets, } from '../../../../containers/detection_engine/rules'; -import { TableData } from '../types'; - -/** - * Formats rules into the correct format for the AllRulesTable - * - * @param rules as returned from the Rules API - * @param selectedIds ids of the currently selected rules - */ -export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] => - rules.map(rule => ({ - id: rule.id, - immutable: rule.immutable, - rule_id: rule.rule_id, - rule: { - href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`, - name: rule.name, - }, - risk_score: rule.risk_score, - severity: rule.severity, - tags: rule.tags ?? [], - activate: rule.enabled, - status: rule.status ?? null, - statusDate: rule.status_date ?? null, - sourceRule: rule, - isLoading: selectedIds?.includes(rule.id) ?? false, - })); /** * Separates rules/errors from bulk rules API response (create/update/delete) @@ -52,14 +26,11 @@ export const bucketRulesResponse = (response: Array) => ); export const showRulesTable = ({ - isInitialLoad, rulesCustomInstalled, rulesInstalled, }: { - isInitialLoad: boolean; rulesCustomInstalled: number | null; rulesInstalled: number | null; }) => - !isInitialLoad && - ((rulesCustomInstalled != null && rulesCustomInstalled > 0) || - (rulesInstalled != null && rulesInstalled > 0)); + (rulesCustomInstalled != null && rulesCustomInstalled > 0) || + (rulesInstalled != null && rulesInstalled > 0); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index b304d77f2e276..79fec526faf48 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -11,15 +11,16 @@ import { EuiLoadingContent, EuiSpacer, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; import uuid from 'uuid'; import { useRules, CreatePreBuiltRules, FilterOptions, + Rule, } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../components/header_section'; import { @@ -36,35 +37,39 @@ import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_em import { RuleDownloader } from '../components/rule_downloader'; import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; -import { EuiBasicTableOnChange, TableData } from '../types'; +import { EuiBasicTableOnChange } from '../types'; import { getBatchItems } from './batch_actions'; import { getColumns } from './columns'; import { showRulesTable } from './helpers'; import { allRulesReducer, State } from './reducer'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; +// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way +// after few hours of fight with typescript !!!! I lost :( +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; + const initialState: State = { - isLoading: true, - rules: [], - tableData: [], - selectedItems: [], - refreshToggle: true, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, + exportRuleIds: [], filterOptions: { filter: '', sortField: 'enabled', sortOrder: 'desc', }, + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + rules: [], + selectedRuleIds: [], }; interface AllRulesProps { createPrePackagedRules: CreatePreBuiltRules | null; hasNoPermissions: boolean; - importCompleteToggle: boolean; loading: boolean; loadingCreatePrePackagedRules: boolean; refetchPrePackagedRulesStatus: () => void; @@ -72,7 +77,7 @@ interface AllRulesProps { rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: () => void) => void; + setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; } /** @@ -87,7 +92,6 @@ export const AllRules = React.memo( ({ createPrePackagedRules, hasNoPermissions, - importCompleteToggle, loading, loadingCreatePrePackagedRules, refetchPrePackagedRulesStatus, @@ -97,24 +101,36 @@ export const AllRules = React.memo( rulesNotUpdated, setRefreshRulesData, }) => { + const [initLoading, setInitLoading] = useState(true); + const tableRef = useRef(); const [ { - exportPayload, + exportRuleIds, filterOptions, - isLoading, - refreshToggle, - selectedItems, - tableData, + loadingRuleIds, + loadingRulesAction, pagination, + rules, + selectedRuleIds, }, dispatch, - ] = useReducer(allRulesReducer, initialState); + ] = useReducer(allRulesReducer(tableRef), initialState); const history = useHistory(); - const [oldRefreshToggle, setOldRefreshToggle] = useState(refreshToggle); - const [isInitialLoad, setIsInitialLoad] = useState(true); - const [isGlobalLoading, setIsGlobalLoad] = useState(false); const [, dispatchToaster] = useStateToaster(); - const [isLoadingRules, rulesData, reFetchRulesData] = useRules(pagination, filterOptions); + + const setRules = useCallback((newRules: Rule[]) => { + dispatch({ + type: 'setRules', + rules: newRules, + }); + }, []); + + const [isLoadingRules, , reFetchRulesData] = useRules({ + pagination, + filterOptions, + refetchPrePackagedRulesStatus, + dispatchRulesInReducer: setRules, + }); const prePackagedRuleStatus = getPrePackagedRuleStatus( rulesInstalled, @@ -125,10 +141,18 @@ export const AllRules = React.memo( const getBatchItemsPopoverContent = useCallback( (closePopover: () => void) => ( ), - [selectedItems, dispatch, dispatchToaster, history] + [dispatch, dispatchToaster, loadingRuleIds, reFetchRulesData, rules, selectedRuleIds] ); const tableOnChangeCallback = useCallback( @@ -146,46 +170,19 @@ export const AllRules = React.memo( ); const columns = useMemo(() => { - return getColumns(dispatch, dispatchToaster, history, hasNoPermissions); - }, [dispatch, dispatchToaster, history]); - - useEffect(() => { - dispatch({ type: 'loading', isLoading: isLoadingRules }); - }, [isLoadingRules]); - - useEffect(() => { - if (!isLoadingRules && !loading && isInitialLoad) { - setIsInitialLoad(false); - } - }, [isInitialLoad, isLoadingRules, loading]); - - useEffect(() => { - if (!isGlobalLoading && (isLoadingRules || isLoading)) { - setIsGlobalLoad(true); - } else if (isGlobalLoading && !isLoadingRules && !isLoading) { - setIsGlobalLoad(false); - } - }, [setIsGlobalLoad, isGlobalLoading, isLoadingRules, isLoading]); - - useEffect(() => { - if (!isInitialLoad) { - dispatch({ type: 'refresh' }); - } - }, [importCompleteToggle]); - - useEffect(() => { - if (!isInitialLoad && reFetchRulesData != null && oldRefreshToggle !== refreshToggle) { - setOldRefreshToggle(refreshToggle); - reFetchRulesData(); - refetchPrePackagedRulesStatus(); - } - }, [ - isInitialLoad, - refreshToggle, - oldRefreshToggle, - reFetchRulesData, - refetchPrePackagedRulesStatus, - ]); + return getColumns({ + dispatch, + dispatchToaster, + history, + hasNoPermissions, + loadingRuleIds: + loadingRulesAction != null && + (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') + ? loadingRuleIds + : [], + reFetchRules: reFetchRulesData, + }); + }, [dispatch, dispatchToaster, history, loadingRuleIds, loadingRulesAction, reFetchRulesData]); useEffect(() => { if (reFetchRulesData != null) { @@ -194,31 +191,25 @@ export const AllRules = React.memo( }, [reFetchRulesData, setRefreshRulesData]); useEffect(() => { - dispatch({ - type: 'updateRules', - rules: rulesData.data, - pagination: { - page: rulesData.page, - perPage: rulesData.perPage, - total: rulesData.total, - }, - }); - }, [rulesData]); + if (initLoading && !loading && !isLoadingRules) { + setInitLoading(false); + } + }, [initLoading, loading, isLoadingRules]); const handleCreatePrePackagedRules = useCallback(async () => { - if (createPrePackagedRules != null) { + if (createPrePackagedRules != null && reFetchRulesData != null) { await createPrePackagedRules(); - dispatch({ type: 'refresh' }); + reFetchRulesData(true); } - }, [createPrePackagedRules]); + }, [createPrePackagedRules, reFetchRulesData]); const euiBasicTableSelectionProps = useMemo( () => ({ - selectable: (item: TableData) => !item.isLoading, - onSelectionChange: (selected: TableData[]) => - dispatch({ type: 'setSelected', selectedItems: selected }), + selectable: (item: Rule) => !loadingRuleIds.includes(item.id), + onSelectionChange: (selected: Rule[]) => + dispatch({ type: 'selectedRuleIds', ids: selected.map(r => r.id) }), }), - [] + [loadingRuleIds] ); const onFilterChangedCallback = useCallback((newFilterOptions: Partial) => { @@ -237,12 +228,25 @@ export const AllRules = React.memo( ); }, []); + const isLoadingAnActionOnRule = useMemo(() => { + if ( + loadingRuleIds.length > 0 && + (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') + ) { + return false; + } else if (loadingRuleIds.length > 0) { + return true; + } + return false; + }, [loadingRuleIds, loadingRulesAction]); + return ( <> { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); dispatchToaster({ type: 'addToaster', toast: { @@ -256,22 +260,17 @@ export const AllRules = React.memo( /> - + <> - {((rulesCustomInstalled && rulesCustomInstalled > 0) || - (rulesInstalled != null && rulesInstalled > 0)) && ( - - - - )} - {isInitialLoad && ( - - )} - {isGlobalLoading && !isEmpty(tableData) && !isInitialLoad && ( + + + + + {(loading || isLoadingRules || isLoadingAnActionOnRule) && !initLoading && ( )} {rulesCustomInstalled != null && @@ -283,7 +282,10 @@ export const AllRules = React.memo( userHasNoPermissions={hasNoPermissions} /> )} - {showRulesTable({ isInitialLoad, rulesCustomInstalled, rulesInstalled }) && ( + {initLoading && ( + + )} + {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && ( <> @@ -292,7 +294,7 @@ export const AllRules = React.memo( - {i18n.SELECTED_RULES(selectedItems.length)} + {i18n.SELECTED_RULES(selectedRuleIds.length)} {!hasNoPermissions && ( ( )} dispatch({ type: 'refresh' })} + onClick={() => reFetchRulesData(true)} > {i18n.REFRESH} - - ( totalItemCount: pagination.total, pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], }} - sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + ref={tableRef} + sorting={{ sort: { field: 'enabled', direction: filterOptions.sortOrder } }} selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts index 3634a16cbf6ac..54da43efd66d9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts @@ -4,34 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiBasicTable } from '@elastic/eui'; import { FilterOptions, PaginationOptions, Rule, } from '../../../../containers/detection_engine/rules'; -import { TableData } from '../types'; -import { formatRules } from './helpers'; +type LoadingRuleAction = 'duplicate' | 'enable' | 'disable' | 'export' | 'delete' | null; export interface State { - isLoading: boolean; - rules: Rule[]; - selectedItems: TableData[]; - pagination: PaginationOptions; + exportRuleIds: string[]; filterOptions: FilterOptions; - refreshToggle: boolean; - tableData: TableData[]; - exportPayload?: Rule[]; + loadingRuleIds: string[]; + loadingRulesAction: LoadingRuleAction; + pagination: PaginationOptions; + rules: Rule[]; + selectedRuleIds: string[]; } export type Action = - | { type: 'refresh' } - | { type: 'loading'; isLoading: boolean } - | { type: 'deleteRules'; rules: Rule[] } - | { type: 'duplicate'; rule: Rule } - | { type: 'setExportPayload'; exportPayload?: Rule[] } - | { type: 'setSelected'; selectedItems: TableData[] } - | { type: 'updateLoading'; ids: string[]; isLoading: boolean } - | { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions } + | { type: 'exportRuleIds'; ids: string[] } + | { type: 'loadingRuleIds'; ids: string[]; actionType: LoadingRuleAction } + | { type: 'selectedRuleIds'; ids: string[] } + | { type: 'setRules'; rules: Rule[] } + | { type: 'updateRules'; rules: Rule[] } | { type: 'updatePagination'; pagination: Partial } | { type: 'updateFilterOptions'; @@ -40,54 +36,71 @@ export type Action = } | { type: 'failure' }; -export const allRulesReducer = (state: State, action: Action): State => { +export const allRulesReducer = ( + tableRef: React.MutableRefObject | undefined> +) => (state: State, action: Action): State => { switch (action.type) { - case 'refresh': { + case 'exportRuleIds': { return { ...state, - refreshToggle: !state.refreshToggle, + loadingRuleIds: action.ids, + loadingRulesAction: 'export', + exportRuleIds: action.ids, }; } - case 'updateRules': { - // If pagination included, this was a hard refresh - if (action.pagination) { - return { - ...state, - rules: action.rules, - pagination: action.pagination, - tableData: formatRules(action.rules), - }; + case 'loadingRuleIds': { + return { + ...state, + loadingRuleIds: action.actionType == null ? [] : [...state.loadingRuleIds, ...action.ids], + loadingRulesAction: action.actionType, + }; + } + case 'selectedRuleIds': { + return { + ...state, + selectedRuleIds: action.ids, + }; + } + case 'setRules': { + if ( + tableRef != null && + tableRef.current != null && + tableRef.current.changeSelection != null + ) { + tableRef.current.changeSelection([]); } - const ruleIds = state.rules.map(r => r.rule_id); - const updatedRules = action.rules.reverse().reduce((rules, updatedRule) => { - let newRules = rules; - if (ruleIds.includes(updatedRule.rule_id)) { - newRules = newRules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)); - } else { - newRules = [...newRules, updatedRule]; - } - return newRules; - }, state.rules); - - // Update enabled on selectedItems so that batch actions show correct available actions - const updatedRuleIdToState = action.rules.reduce>( - (acc, r) => ({ ...acc, [r.id]: r.enabled }), - {} - ); - const updatedSelectedItems = state.selectedItems.map(selectedItem => - Object.keys(updatedRuleIdToState).includes(selectedItem.id) - ? { ...selectedItem, activate: updatedRuleIdToState[selectedItem.id] } - : selectedItem - ); - return { ...state, - rules: updatedRules, - tableData: formatRules(updatedRules), - selectedItems: updatedSelectedItems, + rules: action.rules, + selectedRuleIds: [], + loadingRuleIds: [], + loadingRulesAction: null, }; } + case 'updateRules': { + if (state.rules != null) { + const ruleIds = state.rules.map(r => r.id); + const updatedRules = action.rules.reduce((rules, updatedRule) => { + let newRules = rules; + if (ruleIds.includes(updatedRule.id)) { + newRules = newRules.map(r => (updatedRule.id === r.id ? updatedRule : r)); + } else { + newRules = [...newRules, updatedRule]; + } + return newRules; + }, state.rules); + const updatedRuleIds = action.rules.map(r => r.id); + const newLoadingRuleIds = state.loadingRuleIds.filter(id => !updatedRuleIds.includes(id)); + return { + ...state, + rules: updatedRules, + loadingRuleIds: newLoadingRuleIds, + loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction, + }; + } + return state; + } case 'updatePagination': { return { ...state, @@ -110,51 +123,12 @@ export const allRulesReducer = (state: State, action: Action): State => { }, }; } - case 'deleteRules': { - const deletedRuleIds = action.rules.map(r => r.rule_id); - const updatedRules = state.rules.reduce( - (rules, rule) => (deletedRuleIds.includes(rule.rule_id) ? rules : [...rules, rule]), - [] - ); - return { - ...state, - rules: updatedRules, - tableData: formatRules(updatedRules), - refreshToggle: !state.refreshToggle, - }; - } - case 'setSelected': { - return { - ...state, - selectedItems: action.selectedItems, - }; - } - case 'updateLoading': { - return { - ...state, - rules: state.rules, - tableData: formatRules(state.rules, action.ids), - }; - } - case 'loading': { - return { - ...state, - isLoading: action.isLoading, - }; - } case 'failure': { return { ...state, - isLoading: false, rules: [], }; } - case 'setExportPayload': { - return { - ...state, - exportPayload: [...(action.exportPayload ?? [])], - }; - } default: return state; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index fa4f6a874ca5e..ddb8894c206b5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -41,7 +41,11 @@ const RulesTableFiltersComponent = ({ const [selectedTags, setSelectedTags] = useState([]); const [showCustomRules, setShowCustomRules] = useState(false); const [showElasticRules, setShowElasticRules] = useState(false); - const [isLoadingTags, tags] = useTags(); + const [isLoadingTags, tags, reFetchTags] = useTags(); + + useEffect(() => { + reFetchTags(); + }, [rulesCustomInstalled, rulesInstalled]); // Propagate filter changes to parent useEffect(() => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 88795f9195e68..fbe854c1ee346 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -5,10 +5,10 @@ */ import { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { Filter, @@ -99,7 +99,7 @@ export const QueryBarDefineRule = ({ const newFilters = filterManager.getFilters(); const { filters } = field.value as FieldValueQueryBar; - if (!isEqual(filters, newFilters)) { + if (!deepEqual(filters, newFilters)) { field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); } } @@ -117,10 +117,10 @@ export const QueryBarDefineRule = ({ let isSubscribed = true; async function updateFilterQueryFromValue() { const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; - if (!isEqual(query, queryDraft)) { + if (!deepEqual(query, queryDraft)) { setQueryDraft(query); } - if (!isEqual(filters, filterManager.getFilters())) { + if (!deepEqual(filters, filterManager.getFilters())) { filterManager.setFilters(filters); } if ( @@ -148,7 +148,7 @@ export const QueryBarDefineRule = ({ const onSubmitQuery = useCallback( (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { const { query } = field.value as FieldValueQueryBar; - if (!isEqual(query, newQuery)) { + if (!deepEqual(query, newQuery)) { field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); } }, @@ -158,7 +158,7 @@ export const QueryBarDefineRule = ({ const onChangedQuery = useCallback( (newQuery: Query) => { const { query } = field.value as FieldValueQueryBar; - if (!isEqual(query, newQuery)) { + if (!deepEqual(query, newQuery)) { field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); } }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap index 9bd2fab23ac99..9355d0ae2cccb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap @@ -58,6 +58,7 @@ exports[`RuleActionsOverflow renders correctly against snapshot 1`] = ` `; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx index b52c10881cf42..7c8926c2064c7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx @@ -48,7 +48,7 @@ const RuleActionsOverflowComponent = ({ userHasNoPermissions, }: RuleActionsOverflowComponentProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [rulesToExport, setRulesToExport] = useState(undefined); + const [rulesToExport, setRulesToExport] = useState([]); const history = useHistory(); const [, dispatchToaster] = useStateToaster(); @@ -66,7 +66,7 @@ const RuleActionsOverflowComponent = ({ disabled={userHasNoPermissions} onClick={async () => { setIsPopoverOpen(false); - await duplicateRulesAction([rule], noop, dispatchToaster); + await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster); }} > {i18nActions.DUPLICATE_RULE} @@ -75,9 +75,9 @@ const RuleActionsOverflowComponent = ({ key={i18nActions.EXPORT_RULE} icon="indexEdit" disabled={userHasNoPermissions || rule.immutable} - onClick={async () => { + onClick={() => { setIsPopoverOpen(false); - setRulesToExport([rule]); + setRulesToExport([rule.id]); }} > {i18nActions.EXPORT_RULE} @@ -131,7 +131,7 @@ const RuleActionsOverflowComponent = ({ { displaySuccessToast( i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx index b41265adea6b1..5d3086051a6e2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_downloader/index.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useRef } from 'react'; import styled from 'styled-components'; import { isFunction } from 'lodash/fp'; -import { exportRules, Rule } from '../../../../../containers/detection_engine/rules'; +import { exportRules } from '../../../../../containers/detection_engine/rules'; import { displayErrorToast, useStateToaster } from '../../../../../components/toasters'; import * as i18n from './translations'; @@ -17,7 +17,7 @@ const InvisibleAnchor = styled.a` export interface RuleDownloaderProps { filename: string; - rules?: Rule[]; + ruleIds?: string[]; onExportComplete: (exportCount: number) => void; } @@ -30,7 +30,7 @@ export interface RuleDownloaderProps { */ export const RuleDownloaderComponent = ({ filename, - rules, + ruleIds, onExportComplete, }: RuleDownloaderProps) => { const anchorRef = useRef(null); @@ -41,10 +41,10 @@ export const RuleDownloaderComponent = ({ const abortCtrl = new AbortController(); async function exportData() { - if (anchorRef && anchorRef.current && rules != null) { + if (anchorRef && anchorRef.current && ruleIds != null && ruleIds.length > 0) { try { const exportResponse = await exportRules({ - ruleIds: rules.map(r => r.rule_id), + ruleIds, signal: abortCtrl.signal, }); @@ -61,7 +61,7 @@ export const RuleDownloaderComponent = ({ window.URL.revokeObjectURL(objectURL); } - onExportComplete(rules.length); + onExportComplete(ruleIds.length); } } catch (error) { if (isSubscribed) { @@ -77,7 +77,7 @@ export const RuleDownloaderComponent = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [rules]); + }, [ruleIds]); return ; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx index 2c9173cbeb694..ac457d7345c29 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx @@ -12,8 +12,8 @@ import { EuiLoadingSpinner, EuiText, } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; import { useRuleStatus, RuleInfoStatus } from '../../../../../containers/detection_engine/rules'; import { FormattedDate } from '../../../../../components/formatted_date'; @@ -43,7 +43,7 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]); useEffect(() => { - if (!isEqual(currentStatus, ruleStatus?.current_status)) { + if (!deepEqual(currentStatus, ruleStatus?.current_status)) { setCurrentStatus(ruleStatus?.current_status ?? null); } }, [currentStatus, ruleStatus, setCurrentStatus]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 45da7d081333e..431d793d6e68a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -13,9 +13,9 @@ import { EuiSpacer, EuiButtonEmpty, } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { setFieldValue } from '../../helpers'; import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; @@ -103,7 +103,7 @@ const StepAboutRuleComponent: FC = ({ useEffect(() => { const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { const myDefaultValues = { ...defaultValues, isNew: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 920a9f2dfe56c..773eb44efb26c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -11,9 +11,10 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; -import { isEmpty, isEqual } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import React, { FC, memo, useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; @@ -126,9 +127,9 @@ const StepDefineRuleComponent: FC = ({ useEffect(() => { if (indicesConfig != null && defaultValues != null) { const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues); - if (!isEqual(myDefaultValues, myStepData)) { + if (!deepEqual(myDefaultValues, myStepData)) { setMyStepData(myDefaultValues); - setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); + setLocalUseIndicesConfig(deepEqual(myDefaultValues.index, indicesConfig)); setFieldValue(form, schema, myDefaultValues); } } @@ -212,13 +213,13 @@ const StepDefineRuleComponent: FC = ({ {({ index }) => { if (index != null) { - if (isEqual(index, indicesConfig) && !localUseIndicesConfig) { + if (deepEqual(index, indicesConfig) && !localUseIndicesConfig) { setLocalUseIndicesConfig(true); } - if (!isEqual(index, indicesConfig) && localUseIndicesConfig) { + if (!deepEqual(index, indicesConfig) && localUseIndicesConfig) { setLocalUseIndicesConfig(false); } - if (index != null && !isEmpty(index) && !isEqual(index, mylocalIndicesConfig)) { + if (index != null && !isEmpty(index) && !deepEqual(index, mylocalIndicesConfig)) { setMyLocalIndicesConfig(index); } } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index cfbb0a622c721..2e2c7e068dd85 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -5,8 +5,8 @@ */ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; import { setFieldValue } from '../../helpers'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; @@ -62,7 +62,7 @@ const StepScheduleRuleComponent: FC = ({ useEffect(() => { const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { const myDefaultValues = { ...defaultValues, isNew: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 0c53ad19a3574..20185c2eda816 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -26,11 +26,10 @@ import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/u import { getPrePackagedRuleStatus, redirectToDetections } from './helpers'; import * as i18n from './translations'; -type Func = () => void; +type Func = (refreshPrePackagedRule?: boolean) => void; const RulesPageComponent: React.FC = () => { const [showImportModal, setShowImportModal] = useState(false); - const [importCompleteToggle, setImportCompleteToggle] = useState(false); const refreshRulesData = useRef(null); const { loading, @@ -67,14 +66,18 @@ const RulesPageComponent: React.FC = () => { const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; + const handleRefreshRules = useCallback(async () => { + if (refreshRulesData.current != null) { + refreshRulesData.current(true); + } + }, [refreshRulesData]); + const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { await createPrePackagedRules(); - if (refreshRulesData.current != null) { - refreshRulesData.current(); - } + handleRefreshRules(); } - }, [createPrePackagedRules, refreshRulesData]); + }, [createPrePackagedRules, handleRefreshRules]); const handleRefetchPrePackagedRulesStatus = useCallback(() => { if (refetchPrePackagedRulesStatus != null) { @@ -96,7 +99,7 @@ const RulesPageComponent: React.FC = () => { setShowImportModal(false)} - importComplete={() => setImportCompleteToggle(!importCompleteToggle)} + importComplete={handleRefreshRules} /> { loading={loading || prePackagedRuleLoading} loadingCreatePrePackagedRules={loadingCreatePrePackagedRules} hasNoPermissions={userHasNoPermissions} - importCompleteToggle={importCompleteToggle} refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus} rulesCustomInstalled={rulesCustomInstalled} rulesInstalled={rulesInstalled} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index 55eb45fb5ed9d..b2650dcc2b77e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -5,7 +5,6 @@ */ import { Filter } from '../../../../../../../../src/plugins/data/common'; -import { Rule } from '../../../containers/detection_engine/rules'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from '../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; @@ -23,24 +22,6 @@ export interface EuiBasicTableOnChange { sort?: EuiBasicTableSortTypes; } -export interface TableData { - id: string; - immutable: boolean; - rule_id: string; - rule: { - href: string; - name: string; - }; - risk_score: number; - severity: string; - tags: string[]; - activate: boolean; - isLoading: boolean; - sourceRule: Rule; - status?: string | null; - statusDate?: string | null; -} - export enum RuleStep { defineRule = 'define-rule', aboutRule = 'about-rule', diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx index 41eb620850a7f..f5efd9248029d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx @@ -73,32 +73,25 @@ export const HostDetailsTabs = React.memo( return ( - } - /> - } - /> - } - /> - ( - - )} - /> - } - /> - } - /> + + + + + + + + + + + + + + + + + + + ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx index 0b83710a13293..80c35e5563c1d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; import { HostsTabsProps } from './types'; @@ -22,7 +22,7 @@ import { } from './navigation'; import { HostAlertsQueryTabBody } from './navigation/alerts_query_tab_body'; -const HostsTabs = memo( +export const HostsTabs = memo( ({ deleteQuery, filterQuery, @@ -44,49 +44,48 @@ const HostsTabs = memo( startDate: from, type, indexPattern, - narrowDateRange: (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, + narrowDateRange: useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ), + updateDateRange: useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ), }; return ( - } - /> - } - /> - } - /> - ( - - )} - /> - } - /> - } - /> + + + + + + + + + + + + + + + + + + ); } ); HostsTabs.displayName = 'HostsTabs'; - -export { HostsTabs }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index 23a619db97ee4..b6b54b68ac06a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -23,77 +23,82 @@ import { TlsQueryTabBody } from './tls_query_tab_body'; import { Anomaly } from '../../../components/ml/types'; import { NetworkAlertsQueryTabBody } from './alerts_query_tab_body'; -export const NetworkRoutes = ({ - networkPagePath, - type, - to, - filterQuery, - isInitializing, - from, - indexPattern, - setQuery, - setAbsoluteRangeDatePicker, -}: NetworkRoutesProps) => { - const narrowDateRange = useCallback( - (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - [setAbsoluteRangeDatePicker] - ); +export const NetworkRoutes = React.memo( + ({ + networkPagePath, + type, + to, + filterQuery, + isInitializing, + from, + indexPattern, + setQuery, + setAbsoluteRangeDatePicker, + }) => { + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + const updateDateRange = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); - const networkAnomaliesFilterQuery = { - bool: { - should: [ - { - exists: { - field: 'source.ip', + const networkAnomaliesFilterQuery = { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, }, - }, - { - exists: { - field: 'destination.ip', + { + exists: { + field: 'destination.ip', + }, }, - }, - ], - minimum_should_match: 1, - }, - }; + ], + minimum_should_match: 1, + }, + }; - const commonProps = { - startDate: from, - endDate: to, - skip: isInitializing, - type, - narrowDateRange, - setQuery, - filterQuery, - }; + const commonProps = { + startDate: from, + endDate: to, + skip: isInitializing, + type, + narrowDateRange, + setQuery, + filterQuery, + }; - const tabProps = { - ...commonProps, - indexPattern, - }; + const tabProps = { + ...commonProps, + indexPattern, + updateDateRange, + }; - const anomaliesProps = { - ...commonProps, - anomaliesFilterQuery: networkAnomaliesFilterQuery, - AnomaliesTableComponent: AnomaliesNetworkTable, - }; + const anomaliesProps = { + ...commonProps, + anomaliesFilterQuery: networkAnomaliesFilterQuery, + AnomaliesTableComponent: AnomaliesNetworkTable, + }; - return ( - - } - /> - ( + return ( + + + + + <> @@ -118,31 +123,25 @@ export const NetworkRoutes = ({ - )} - /> - } - /> - } - /> - ( + + + + + + + + - )} - /> - } - /> - - ); -}; + + + + + + ); + } +); NetworkRoutes.displayName = 'NetworkRoutes'; diff --git a/x-pack/legacy/plugins/siem/public/routes.tsx b/x-pack/legacy/plugins/siem/public/routes.tsx index cbb58a473e8ea..a989fa9873435 100644 --- a/x-pack/legacy/plugins/siem/public/routes.tsx +++ b/x-pack/legacy/plugins/siem/public/routes.tsx @@ -20,8 +20,12 @@ const PageRouterComponent: FC = ({ history }) => ( - } /> - } /> + + + + + + diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx index 213b881bd2084..af993588f7e0d 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; import { Dispatch } from 'redux'; import { IIndexPattern } from 'src/plugins/data/public'; +import deepEqual from 'fast-deep-equal'; import { KueryFilterQuery } from '../../store'; import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../store/timeline/actions'; @@ -29,7 +29,7 @@ export const useUpdateKql = ({ timelineId, }: UseUpdateKqlProps): RefetchKql => { const updateKql: RefetchKql = (dispatch: Dispatch) => { - if (kueryFilterQueryDraft != null && !isEqual(kueryFilterQuery, kueryFilterQueryDraft)) { + if (kueryFilterQueryDraft != null && !deepEqual(kueryFilterQuery, kueryFilterQueryDraft)) { if (storeType === 'timelineType' && timelineId != null) { dispatch( dispatchApplyTimelineFilterQuery({ diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx index c88562abef6ae..ddee2359b28ba 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx @@ -5,7 +5,6 @@ */ import * as H from 'history'; -import { isEqual } from 'lodash/fp'; import { memo, useEffect, useState } from 'react'; import { withRouter } from 'react-router-dom'; import deepEqual from 'fast-deep-equal'; @@ -35,7 +34,7 @@ export const SpyRouteComponent = memo( } }, [search]); useEffect(() => { - if (pageName && !isEqual(route.pathName, pathname)) { + if (pageName && !deepEqual(route.pathName, pathname)) { if (isInitializing && detailName == null) { dispatch({ type: 'updateRouteWithOutSearch', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 1578c71dddc6a..2b50011cf4dff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -427,13 +427,54 @@ export const getMockPrivileges = () => ({ has_encryption_key: true, }); -export const getFindResultStatus = (): SavedObjectsFindResponse => ({ +export const getFindResultStatusEmpty = (): SavedObjectsFindResponse => ({ page: 1, per_page: 1, total: 0, saved_objects: [], }); +export const getFindResultStatus = (): SavedObjectsFindResponse => ({ + page: 1, + per_page: 6, + total: 2, + saved_objects: [ + { + type: 'my-type', + id: 'e0b86950-4e9f-11ea-bdbd-07b56aa159b3', + attributes: { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:26:49.783Z', + status: 'succeeded', + lastFailureAt: null, + lastSuccessAt: '2020-02-18T15:26:49.783Z', + lastFailureMessage: null, + lastSuccessMessage: 'succeeded', + }, + references: [], + updated_at: '2020-02-18T15:26:51.333Z', + version: 'WzQ2LDFd', + }, + { + type: 'my-type', + id: '91246bd0-5261-11ea-9650-33b954270f67', + attributes: { + alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', + statusDate: '2020-02-18T15:15:58.806Z', + status: 'failed', + lastFailureAt: '2020-02-18T15:15:58.806Z', + lastSuccessAt: '2020-02-13T20:31:59.855Z', + lastFailureMessage: + 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', + lastSuccessMessage: 'succeeded', + }, + references: [], + updated_at: '2020-02-18T15:15:58.860Z', + version: 'WzMyLDFd', + }, + ], +}); + export const getIndexName = () => 'index-name'; export const getEmptyIndex = (): { _shards: Partial } => ({ _shards: { total: 0 }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts index f8c8e1f231ffa..32226e38a1f7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -26,6 +26,20 @@ export const getSimpleRule = (ruleId = 'rule-1'): Partial = query: 'user.name: root or user.name: admin', }); +/** + * This is a typical simple rule for testing that is easy for most basic testing + * @param ruleId + */ +export const getSimpleRuleWithId = (id = 'rule-1'): Partial => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + id, + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', +}); + /** * Given an array of rule_id strings this will return a ndjson buffer which is useful * for testing uploads. @@ -51,3 +65,26 @@ export const getSimpleRuleAsMultipartContent = (ruleIds: string[], isNdjson = tr return Buffer.from(resultingPayload); }; + +/** + * Given an array of rule_id strings this will return a ndjson buffer which is useful + * for testing uploads. + * @param count Number of rules to generate + * @param isNdjson Boolean to determine file extension + */ +export const getSimpleRuleAsMultipartContentNoRuleId = (count: number, isNdjson = true): Buffer => { + const arrayOfRules = Array(count).fill(JSON.stringify(getSimpleRuleWithId())); + const stringOfRules = arrayOfRules.join('\r\n'); + + const resultingPayload = + `--${TEST_BOUNDARY}\r\n` + + `Content-Disposition: form-data; name="file"; filename="rules.${ + isNdjson ? 'ndjson' : 'json' + }\r\n` + + 'Content-Type: application/octet-stream\r\n' + + '\r\n' + + `${stringOfRules}\r\n` + + `--${TEST_BOUNDARY}--\r\n`; + + return Buffer.from(resultingPayload); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 41be42f7c0fe1..26a6c790ceef9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -36,25 +36,14 @@ export const createReadIndexRoute = ( const indexExists = await getIndexExists(callCluster, index); if (indexExists) { - // head request is used for if you want to get if the index exists - // or not and it will return a content-length: 0 along with either a 200 or 404 - // depending on if the index exists or not. - if (request.method.toLowerCase() === 'head') { - return headers.response().code(200); - } else { - return headers.response({ name: index }).code(200); - } + return headers.response({ name: index }).code(200); } else { - if (request.method.toLowerCase() === 'head') { - return headers.response().code(404); - } else { - return headers - .response({ - message: 'index for this space does not exist', - status_code: 404, - }) - .code(404); - } + return headers + .response({ + message: 'index for this space does not exist', + status_code: 404, + }) + .code(404); } } catch (err) { const error = transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 308ee95a77e20..3c31658c61d6e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -5,6 +5,7 @@ */ import { readPrivilegesRoute } from './read_privileges_route'; +import * as readPrivileges from '../../privileges/read_privileges'; import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; @@ -38,5 +39,16 @@ describe('read_privileges', () => { const { payload } = await inject(getPrivilegeRequest()); expect(JSON.parse(payload)).toEqual(getMockPrivileges()); }); + + test('returns 500 when bad response from readPrivileges', async () => { + jest.spyOn(readPrivileges, 'readPrivileges').mockImplementation(() => { + throw new Error('Test error'); + }); + const { payload } = await inject(getPrivilegeRequest()); + expect(JSON.parse(payload)).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index e018ed4cc22ff..e6a93fdadcfca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -6,7 +6,6 @@ import { omit } from 'lodash/fp'; -import { createRulesRoute } from './create_rules_route'; import { getFindResult, getResult, @@ -17,6 +16,7 @@ import { getNonEmptyIndex, } from '../__mocks__/request_responses'; import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; +import * as updatePrepackagedRules from '../../rules/update_prepacked_rules'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -54,7 +54,8 @@ describe('add_prepackaged_rules_route', () => { beforeEach(() => { jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); getClients = clientsServiceMock.createGetScoped(); @@ -78,9 +79,7 @@ describe('add_prepackaged_rules_route', () => { test('returns 404 if alertClient is not available on the route', async () => { getClients.mockResolvedValue(omit('alertsClient', clients)); - const { inject, route } = createMockServer(); - createRulesRoute(route, config, getClients); - const { statusCode } = await inject(addPrepackagedRulesRequest()); + const { statusCode } = await server.inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(404); }); }); @@ -126,5 +125,19 @@ describe('add_prepackaged_rules_route', () => { rules_updated: 1, }); }); + test('catches errors if payloads cause errors to be thrown', async () => { + jest.spyOn(updatePrepackagedRules, 'updatePrepackagedRules').mockImplementation(() => { + throw new Error('Test error'); + }); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + const { payload } = await server.inject(addPrepackagedRulesRequest()); + expect(JSON.parse(payload)).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 664d27a7572ad..931623ea6652c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -14,12 +14,15 @@ import { typicalPayload, getReadBulkRequest, getEmptyIndex, + getNonEmptyIndex, } from '../__mocks__/request_responses'; import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesBulkRoute } from './create_rules_bulk_route'; import { BulkError } from '../utils'; import { OutputRuleAlertRest } from '../../types'; +import * as createRules from '../../rules/create_rules'; +import * as readRules from '../../rules/read_rules'; describe('create_rules_bulk', () => { let server = createMockServer(); @@ -29,10 +32,14 @@ describe('create_rules_bulk', () => { beforeEach(() => { jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + getClients.mockResolvedValue(clients); createRulesBulkRoute(server.route, config, getClients); @@ -44,8 +51,12 @@ describe('create_rules_bulk', () => { clients.alertsClient.get.mockResolvedValue(getResult()); clients.actionsClient.create.mockResolvedValue(createActionResult()); clients.alertsClient.create.mockResolvedValue(getResult()); - const { statusCode } = await server.inject(getReadBulkRequest()); + jest.spyOn(createRules, 'createRules').mockImplementation(async () => { + return getResult(); + }); + const { payload, statusCode } = await server.inject(getReadBulkRequest()); expect(statusCode).toBe(200); + expect(JSON.parse(payload).error).toBeUndefined(); }); test('returns 404 if alertClient is not available on the route', async () => { @@ -149,6 +160,24 @@ describe('create_rules_bulk', () => { expect(output.some(item => item.error?.status_code === 409)).toBeTruthy(); }); + test('returns 409 if duplicate rule_ids found in rule saved objects', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + return getResult(); + }); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array> = JSON.parse(payload); + expect(output.some(item => item.error?.status_code === 409)).toBeTruthy(); + }); + test('returns one error object in response when duplicate rule_ids found in request payload', async () => { clients.alertsClient.find.mockResolvedValue(getFindResult()); clients.alertsClient.get.mockResolvedValue(getResult()); @@ -163,4 +192,22 @@ describe('create_rules_bulk', () => { const output: Array> = JSON.parse(payload); expect(output.length).toBe(1); }); + + test('catches error if createRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest.spyOn(createRules, 'createRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array> = JSON.parse(payload); + expect(output[0].error.message).toBe('Test error'); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 51b7b132fc794..08a0589389966 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import { countBy } from 'lodash/fp'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -45,8 +44,7 @@ export const createCreateRulesBulkRoute = ( } const ruleDefinitions = request.payload; - const mappedDuplicates = countBy('rule_id', ruleDefinitions); - const dupes = getDuplicates(mappedDuplicates); + const dupes = getDuplicates(ruleDefinitions, 'rule_id'); const rules = await Promise.all( ruleDefinitions diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 4f28771db8ed7..5ad43e70f2651 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -9,6 +9,9 @@ import { omit } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesRoute } from './create_rules_route'; +import * as createRules from '../../rules/create_rules'; +import * as readRules from '../../rules/read_rules'; +import * as utils from './utils'; import { getFindResult, @@ -29,8 +32,12 @@ describe('create_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); getClients = clientsServiceMock.createGetScoped(); @@ -130,5 +137,44 @@ describe('create_rules', () => { const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); + + test('catches error if createRules throws error', async () => { + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(createRules, 'createRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getCreateRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); + + test('catches error if transform returns null', async () => { + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getCreateRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('returns 409 if duplicate rule_ids found in rule saved objects', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + return getResult(); + }); + const { payload } = await server.inject(getCreateRequest()); + const output = JSON.parse(payload); + expect(output.status_code).toEqual(409); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 855bf7f634c26..fb44f96d76859 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -48,6 +48,16 @@ describe('delete_rules', () => { expect(statusCode).toBe(200); }); + test('resturns 200 when deleting a single rule and related rule status', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue(true); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteBulkRequest()); + expect(statusCode).toBe(200); + }); + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId using POST', async () => { clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 6438318cb43db..aabf3e513bfea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -58,7 +58,7 @@ export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.S ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - return transformOrBulkError(idOrRuleIdOrUnknown, rule); + return transformOrBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index a0a6f61223279..57c7c85976619 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -7,6 +7,8 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; import { deleteRulesRoute } from './delete_rules_route'; +import * as utils from './utils'; +import * as deleteRules from '../../rules/delete_rules'; import { getFindResult, @@ -25,7 +27,12 @@ describe('delete_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); @@ -73,6 +80,32 @@ describe('delete_rules', () => { const { statusCode } = await inject(getDeleteRequest()); expect(statusCode).toBe(404); }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getDeleteRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if deleteRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); + jest.spyOn(deleteRules, 'deleteRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getDeleteRequestById()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 5b75f17164acf..019424ea2420a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -6,13 +6,22 @@ import { omit } from 'lodash/fp'; +import { + getFindResult, + getResult, + getFindResultWithSingleHit, + getFindResultStatus, + getFindRequest, +} from '../__mocks__/request_responses'; import { createMockServer } from '../__mocks__'; import { clientsServiceMock } from '../__mocks__/clients_service_mock'; +import * as utils from './utils'; +import * as findRules from '../../rules/find_rules'; + import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('find_rules', () => { @@ -21,7 +30,12 @@ describe('find_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); @@ -33,15 +47,10 @@ describe('find_rules', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { - clients.alertsClient.find.mockResolvedValue(getFindResult()); - clients.actionsClient.find.mockResolvedValue({ - page: 1, - perPage: 1, - total: 0, - data: [], - }); + test('returns 200 when finding a single rule with a valid alertsClient', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getFindRequest()); expect(statusCode).toBe(200); }); @@ -53,6 +62,28 @@ describe('find_rules', () => { const { statusCode } = await inject(getFindRequest()); expect(statusCode).toBe(404); }); + + test('catches error when transformation fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transformFindAlerts').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getFindRequest()); + expect(statusCode).toBe(500); + expect(JSON.parse(payload).message).toBe('unknown data type, error transforming alert'); + }); + + test('catch error when findRules function throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(findRules, 'findRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getFindRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts new file mode 100644 index 0000000000000..00411c550fa2e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { omit } from 'lodash/fp'; + +import { getFindResultStatus } from '../__mocks__/request_responses'; +import { createMockServer } from '../__mocks__'; +import { clientsServiceMock } from '../__mocks__/clients_service_mock'; + +import { findRulesStatusesRoute } from './find_rules_status_route'; +import { ServerInjectOptions } from 'hapi'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; + +describe('find_statuses', () => { + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + findRulesStatusesRoute(server.route, getClients); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when finding a single rule status with a valid alertsClient', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + getClients.mockResolvedValue(omit('alertsClient', clients)); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(404); + }); + + test('catch error when savedObjectsClient find function throws error', async () => { + clients.savedObjectsClient.find.mockImplementation(async () => { + throw new Error('Test error'); + }); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { payload, statusCode } = await server.inject(request); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); + }); + + describe('validation', () => { + test('returns 400 if id is given instead of ids', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?id=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 200 if the set of optional query parameters are given', async () => { + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'GET', + url: `${DETECTION_ENGINE_RULES_URL}/_find_statuses?ids=["someid"]`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index fe8742ff0b60c..c496c7b7ce59c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import { snakeCase } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { LegacyServices, LegacyRequest } from '../../../../types'; @@ -18,17 +17,7 @@ import { IRuleStatusAttributes, } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const convertToSnakeCase = >(obj: T): Partial | null => { - if (!obj) { - return null; - } - return Object.keys(obj).reduce((acc, item) => { - const newKey = snakeCase(item); - return { ...acc, [newKey]: obj[item] }; - }, {}); -}; +import { transformError, convertToSnakeCase } from '../utils'; export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', @@ -57,33 +46,43 @@ export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.S "anotherAlertId": ... } */ - const statuses = await query.ids.reduce>(async (acc, id) => { - const lastFiveErrorsForId = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 6, - sortField: 'statusDate', - sortOrder: 'desc', - search: id, - searchFields: ['alertId'], - }); - const accumulated = await acc; - const currentStatus = convertToSnakeCase( - lastFiveErrorsForId.saved_objects[0]?.attributes - ); - const failures = lastFiveErrorsForId.saved_objects - .slice(1) - .map(errorItem => convertToSnakeCase(errorItem.attributes)); - return { - ...accumulated, - [id]: { - current_status: currentStatus, - failures, - }, - }; - }, Promise.resolve({})); - return statuses; + try { + const statuses = await query.ids.reduce>(async (acc, id) => { + const lastFiveErrorsForId = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 6, + sortField: 'statusDate', + sortOrder: 'desc', + search: id, + searchFields: ['alertId'], + }); + const accumulated = await acc; + const currentStatus = convertToSnakeCase( + lastFiveErrorsForId.saved_objects[0]?.attributes + ); + const failures = lastFiveErrorsForId.saved_objects + .slice(1) + .map(errorItem => convertToSnakeCase(errorItem.attributes)); + return { + ...accumulated, + [id]: { + current_status: currentStatus, + failures, + }, + }; + }, Promise.resolve({})); + return statuses; + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index 8f27910a7e5e2..99157b4d15360 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash/fp'; import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; +import * as findRules from '../../rules/find_rules'; import { getFindResult, @@ -47,7 +48,12 @@ describe('get_prepackaged_rule_status_route', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); @@ -76,6 +82,17 @@ describe('get_prepackaged_rule_status_route', () => { const { statusCode } = await inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(404); }); + + test('catch error when findRules function throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + jest.spyOn(findRules, 'findRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getPrepackagedRulesStatusRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('payload', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index b1dd08f8ca371..c8b77e505b5d7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -8,6 +8,7 @@ import { omit } from 'lodash/fp'; import { getSimpleRuleAsMultipartContent, + getSimpleRuleAsMultipartContentNoRuleId, TEST_BOUNDARY, UNPARSABLE_LINE, getSimpleRule, @@ -25,6 +26,7 @@ import { import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { importRulesRoute } from './import_rules_route'; import { DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants'; +import * as createRulesStreamFromNdJson from '../../rules/create_rules_stream_from_ndjson'; describe('import_rules_route', () => { let server = createMockServer(); @@ -33,8 +35,12 @@ describe('import_rules_route', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); config = () => ({ @@ -94,6 +100,21 @@ describe('import_rules_route', () => { const { statusCode } = await inject(getImportRulesRequest(requestPayload)); expect(statusCode).toEqual(404); }); + + test('returns error if createPromiseFromStreams throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + jest + .spyOn(createRulesStreamFromNdJson, 'createRulesStreamFromNdJson') + .mockImplementation(() => { + throw new Error('Test error'); + }); + const requestPayload = getSimpleRuleAsMultipartContent(['rule-1']); + const { payload, statusCode } = await server.inject(getImportRulesRequest(requestPayload)); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { @@ -306,6 +327,21 @@ describe('import_rules_route', () => { expect(statusCode).toEqual(200); }); + test('returns 200 with errors if all rules are missing rule_ids and import fails on validation', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + + const requestPayload = getSimpleRuleAsMultipartContentNoRuleId(2); + const { statusCode, payload } = await server.inject(getImportRulesRequest(requestPayload)); + const parsed: ImportSuccessError = JSON.parse(payload); + + expect(parsed.success).toEqual(false); + expect(parsed.errors[0].error.message).toEqual( + 'child "rule_id" fails because ["rule_id" is required]' + ); + expect(parsed.errors[0].error.status_code).toEqual(400); + expect(statusCode).toEqual(200); + }); + test('returns 200 with reported conflict if error parsing rule', async () => { const multipartPayload = `--${TEST_BOUNDARY}\r\n` + diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index f438e0120f96a..a9358a47f25fc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -7,6 +7,7 @@ import Hapi from 'hapi'; import { chunk, isEmpty } from 'lodash/fp'; import { extname } from 'path'; +import { Readable } from 'stream'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -15,7 +16,7 @@ import { createRules } from '../../rules/create_rules'; import { ImportRulesRequest } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; import { getIndexExists } from '../../index/get_index_exists'; -import { getIndex, createBulkErrorObject, ImportRuleResponse } from '../utils'; +import { getIndex, transformError, createBulkErrorObject, ImportRuleResponse } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; @@ -74,179 +75,192 @@ export const createImportRulesRoute = ( } const objectLimit = config().get('savedObjects.maxImportExportSize'); - const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); - const parsedObjects = await createPromiseFromStreams([readStream]); - const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueRules( - parsedObjects, - request.query.overwrite - ); + try { + const readStream = createRulesStreamFromNdJson(objectLimit); + const parsedObjects = await createPromiseFromStreams([ + request.payload.file as Readable, + ...readStream, + ]); + const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueRules( + parsedObjects, + request.query.overwrite + ); - const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); - let importRuleResponse: ImportRuleResponse[] = []; + const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); + let importRuleResponse: ImportRuleResponse[] = []; - while (chunkParseObjects.length) { - const batchParseObjects = chunkParseObjects.shift() ?? []; - const newImportRuleResponse = await Promise.all( - batchParseObjects.reduce>>((accum, parsedRule) => { - const importsWorkerPromise = new Promise( - async (resolve, reject) => { - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedRule.message, - }) - ); - return null; - } - const { - description, - enabled, - false_positives: falsePositives, - from, - immutable, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - } = parsedRule; - try { - const finalIndex = getIndex(spacesClient.getSpaceId, config); - const indexExists = await getIndexExists( - clusterClient.callAsCurrentUser, - finalIndex - ); - if (!indexExists) { + while (chunkParseObjects.length) { + const batchParseObjects = chunkParseObjects.shift() ?? []; + const newImportRuleResponse = await Promise.all( + batchParseObjects.reduce>>((accum, parsedRule) => { + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId resolve( createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + statusCode: 400, + message: parsedRule.message, }) ); + return null; } - const rule = await readRules({ alertsClient, ruleId }); - if (rule == null) { - await createRules({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null && request.query.overwrite) { - await patchRules({ - alertsClient, - actionsClient, - savedObjectsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - id: undefined, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null) { + const { + description, + enabled, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + } = parsedRule; + try { + const finalIndex = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + finalIndex + ); + if (!indexExists) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }) + ); + } + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null && request.query.overwrite) { + await patchRules({ + alertsClient, + actionsClient, + savedObjectsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }) + ); + } + } catch (err) { resolve( createBulkErrorObject({ ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, + statusCode: 400, + message: err.message, }) ); } - } catch (err) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: 400, - message: err.message, - }) - ); } - } - ); - return [...accum, importsWorkerPromise]; - }, []) - ); - importRuleResponse = [ - ...duplicateIdErrors, - ...importRuleResponse, - ...newImportRuleResponse, - ]; - } + ); + return [...accum, importsWorkerPromise]; + }, []) + ); + importRuleResponse = [ + ...duplicateIdErrors, + ...importRuleResponse, + ...newImportRuleResponse, + ]; + } - const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); - return { - success: errorsResp.length === 0, - success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, - errors: errorsResp, - }; + const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); + return { + success: errorsResp.length === 0, + success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, + errors: errorsResp, + }; + } catch (exc) { + const error = transformError(exc); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts index 02af4135b534f..1cfb4ae81ab85 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts @@ -15,6 +15,7 @@ import { typicalPayload, getFindResultWithSingleHit, getPatchBulkRequest, + getFindResultStatus, } from '../__mocks__/request_responses'; import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -43,6 +44,7 @@ describe('patch_rules_bulk', () => { clients.alertsClient.get.mockResolvedValue(getResult()); clients.actionsClient.update.mockResolvedValue(updateActionResult()); clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); @@ -56,6 +58,13 @@ describe('patch_rules_bulk', () => { expect(statusCode).toBe(200); }); + test('returns 404 as a response when missing alertsClient', async () => { + getClients.mockResolvedValue(omit('alertsClient', clients)); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + const { statusCode } = await server.inject(getPatchBulkRequest()); + expect(statusCode).toBe(404); + }); + test('returns 404 within the payload when updating a single rule that does not exist', async () => { clients.alertsClient.find.mockResolvedValue(getFindResult()); clients.alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index cc84b08fdef11..04cd3a026562f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -7,6 +7,8 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; import { patchRulesRoute } from './patch_rules_route'; +import * as utils from './utils'; +import * as patchRules from '../../rules/patch_rules'; import { getFindResult, @@ -26,7 +28,13 @@ describe('patch_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); @@ -63,6 +71,32 @@ describe('patch_rules', () => { const { statusCode } = await inject(getPatchRequest()); expect(statusCode).toBe(404); }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getPatchRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if patchRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(patchRules, 'patchRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getPatchRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 7c4653af97f21..0366d3648e1ea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -7,6 +7,9 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; +import * as utils from './utils'; +import * as readRules from '../../rules/read_rules'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { readRulesRoute } from './read_rules_route'; import { @@ -24,8 +27,12 @@ describe('read_signals', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); - + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); getClients = clientsServiceMock.createGetScoped(); clients = clientsServiceMock.createClients(); @@ -50,6 +57,38 @@ describe('read_signals', () => { const { statusCode } = await inject(getReadRequest()); expect(statusCode).toBe(404); }); + + test('returns error if readRules returns null', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(readRules, 'readRules').mockResolvedValue(null); + const { payload, statusCode } = await server.inject(getReadRequest()); + expect(JSON.parse(payload).message).toBe('rule_id: "rule-1" not found'); + expect(statusCode).toBe(404); + }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getReadRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if readRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getReadRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts index 9ff7ebc37aab1..32a633799ad44 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -75,9 +75,9 @@ describe('update_rules_bulk', () => { test('returns 404 if alertClient is not available on the route', async () => { getClients.mockResolvedValue(omit('alertsClient', clients)); - const { route, inject } = createMockServer(); + const { route } = createMockServer(); updateRulesRoute(route, config, getClients); - const { statusCode } = await inject(getUpdateBulkRequest()); + const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 7cadfa94467a7..c3a92ed9a61ae 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -7,6 +7,9 @@ import { ServerInjectOptions } from 'hapi'; import { omit } from 'lodash/fp'; +import * as utils from './utils'; +import * as updateRules from '../../rules/update_rules'; + import { updateRulesRoute } from './update_rules_route'; import { getFindResult, @@ -27,7 +30,12 @@ describe('update_rules', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); server = createMockServer(); config = createMockConfig(); @@ -66,6 +74,28 @@ describe('update_rules', () => { const { statusCode } = await inject(getUpdateRequest()); expect(statusCode).toBe(404); }); + + test('returns 500 when transform fails', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(utils, 'transform').mockReturnValue(null); + const { payload, statusCode } = await server.inject(getUpdateRequest()); + expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(statusCode).toBe(500); + }); + + test('catches error if readRules throws error', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + jest.spyOn(updateRules, 'updateRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getUpdateRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 593c55bcae9f2..5fac3f79f359c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -20,7 +20,7 @@ import { } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types'; +import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { sampleRule } from '../../signals/__mocks__/es_results'; import { getSimpleRule } from '../__mocks__/utils'; @@ -1222,20 +1222,32 @@ describe('utils', () => { describe('getDuplicates', () => { test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => { - const output = getDuplicates({ - value1: 1, - value2: 2, - value3: 2, - }); + const output = getDuplicates( + [ + { rule_id: 'value1' }, + { rule_id: 'value2' }, + { rule_id: 'value2' }, + { rule_id: 'value3' }, + { rule_id: 'value3' }, + {}, + {}, + ] as RuleAlertParamsRest[], + 'rule_id' + ); const expected = ['value2', 'value3']; expect(output).toEqual(expected); }); test('returns null when given a map of no duplicates', () => { - const output = getDuplicates({ - value1: 1, - value2: 1, - value3: 1, - }); + const output = getDuplicates( + [ + { rule_id: 'value1' }, + { rule_id: 'value2' }, + { rule_id: 'value3' }, + {}, + {}, + ] as RuleAlertParamsRest[], + 'rule_id' + ); const expected: string[] = []; expect(output).toEqual(expected); }); @@ -1251,9 +1263,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); const isInstanceOfError = output[0] instanceof Error; @@ -1272,9 +1285,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); @@ -1290,6 +1304,30 @@ describe('utils', () => { ]); }); + test('returns tuple of duplicate conflict error and single rule when rules with matching ids passed in and `overwrite` is false', async () => { + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + const rule2 = getSimpleRule('rule-1'); + delete rule2.rule_id; + const ndJsonStream = new Readable({ + read() { + this.push(`${JSON.stringify(rule)}\n`); + this.push(`${JSON.stringify(rule2)}\n`); + this.push(null); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const parsedObjects = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); + const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); + const isInstanceOfError = output[0] instanceof Error; + + expect(isInstanceOfError).toEqual(true); + expect(errors).toEqual([]); + }); + test('returns tuple of empty duplicate errors array and single rule when rules with matching rule-ids passed in and `overwrite` is true', async () => { const rule = getSimpleRule('rule-1'); const rule2 = getSimpleRule('rule-1'); @@ -1300,9 +1338,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, true); @@ -1320,9 +1359,10 @@ describe('utils', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); const parsedObjects = await createPromiseFromStreams([ - rulesObjectsStream, + ndJsonStream, + ...rulesObjectsStream, ]); const [errors, output] = getTupleDuplicateErrorsAndUniqueRules(parsedObjects, false); const isInstanceOfError = output[0] instanceof Error; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 198cdbfb9771d..7004bf2088ef2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pickBy } from 'lodash/fp'; -import { Dictionary } from 'lodash'; +import { pickBy, countBy } from 'lodash/fp'; import { SavedObject } from 'kibana/server'; import uuid from 'uuid'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; @@ -18,7 +17,7 @@ import { isRuleStatusFindTypes, isRuleStatusSavedObjectType, } from '../../rules/types'; -import { OutputRuleAlertRest, ImportRuleAlertRest } from '../../types'; +import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; import { createBulkErrorObject, BulkError, @@ -180,9 +179,7 @@ export const transform = ( if (!ruleStatus && isAlertType(alert)) { return transformAlertToRule(alert); } - if (isAlertType(alert) && isRuleStatusFindType(ruleStatus)) { - return transformAlertToRule(alert, ruleStatus.saved_objects[0]); - } else if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) { + if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) { return transformAlertToRule(alert, ruleStatus); } else { return null; @@ -195,7 +192,7 @@ export const transformOrBulkError = ( ruleStatus?: unknown ): Partial | BulkError => { if (isAlertType(alert)) { - if (isRuleStatusFindType(ruleStatus)) { + if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { return transformAlertToRule(alert, ruleStatus?.saved_objects[0] ?? ruleStatus); } else { return transformAlertToRule(alert); @@ -226,10 +223,14 @@ export const transformOrImportError = ( } }; -export const getDuplicates = (lodashDict: Dictionary): string[] => { - const hasDuplicates = Object.values(lodashDict).some(i => i > 1); +export const getDuplicates = (ruleDefinitions: RuleAlertParamsRest[], by: 'rule_id'): string[] => { + const mappedDuplicates = countBy( + by, + ruleDefinitions.filter(r => r[by] != null) + ); + const hasDuplicates = Object.values(mappedDuplicates).some(i => i > 1); if (hasDuplicates) { - return Object.keys(lodashDict).filter(key => lodashDict[key] > 1); + return Object.keys(mappedDuplicates).filter(key => mappedDuplicates[key] > 1); } return []; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 3e7ed4de6d8c6..7086c62f81711 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -25,7 +25,12 @@ describe('set signal status', () => { let clients = clientsServiceMock.createClients(); beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); server = createMockServer(); @@ -50,6 +55,15 @@ describe('set signal status', () => { const { statusCode } = await server.inject(getSetSignalStatusByQueryRequest()); expect(statusCode).toBe(200); }); + + test('catches error if callAsCurrentUser throws error', async () => { + clients.clusterClient.callAsCurrentUser.mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(getSetSignalStatusByQueryRequest()); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); describe('validation', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index dd3b8d3c99e0c..ee3fd349a26ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -28,7 +28,7 @@ export const setSignalsStatusRouteDef = ( payload: setSignalsStatusSchema, }, }, - async handler(request: SignalsStatusRequest) { + async handler(request: SignalsStatusRequest, headers) { const { signal_ids: signalIds, query, status } = request.payload; const { clusterClient, spacesClient } = await getClients(request); const index = getIndex(spacesClient.getSpaceId, config); @@ -45,7 +45,7 @@ export const setSignalsStatusRouteDef = ( }; } try { - return clusterClient.callAsCurrentUser('updateByQuery', { + const updateByQueryResponse = await clusterClient.callAsCurrentUser('updateByQuery', { index, body: { script: { @@ -56,9 +56,15 @@ export const setSignalsStatusRouteDef = ( }, ignoreUnavailable: true, }); - } catch (exc) { - // error while getting or updating signal with id: id in signal index .siem-signals - return transformError(exc); + return updateByQueryResponse; + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 9439adfcec3cb..210ac9f3d7b01 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -127,5 +127,18 @@ describe('query for signal', () => { const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); + test('catches error if deleteRules throws error', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }, + }; + clients.clusterClient.callAsCurrentUser.mockImplementation(async () => { + throw new Error('Test error'); + }); + const { payload, statusCode } = await server.inject(request); + expect(JSON.parse(payload).message).toBe('Test error'); + expect(statusCode).toBe(500); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index adb6e5f32921a..7636329ecc306 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -28,21 +28,27 @@ export const querySignalsRouteDef = ( payload: querySignalsSchema, }, }, - async handler(request: SignalsQueryRequest) { + async handler(request: SignalsQueryRequest, headers) { const { query, aggs, _source, track_total_hits, size } = request.payload; const { clusterClient, spacesClient } = await getClients(request); const index = getIndex(spacesClient.getSpaceId, config); try { - return clusterClient.callAsCurrentUser('search', { + const searchSignalsIndexResult = await clusterClient.callAsCurrentUser('search', { index, body: { query, aggs, _source, track_total_hits, size }, ignoreUnavailable: true, }); - } catch (exc) { - // error while getting or updating signal with id: id in signal index .siem-signals - return transformError(exc); + return searchSignalsIndexResult; + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 957ddd4ee6caa..3148083b4db26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -15,6 +15,7 @@ import { ImportSuccessError, createImportErrorObject, transformImportError, + convertToSnakeCase, } from './utils'; import { createMockConfig } from './__mocks__'; @@ -312,4 +313,15 @@ describe('utils', () => { expect(index).toEqual('mockSignalsIndex-myspace'); }); }); + + describe('convertToSnakeCase', () => { + it('converts camelCase to snakeCase', () => { + const values = { myTestCamelCaseKey: 'something' }; + expect(convertToSnakeCase(values)).toEqual({ my_test_camel_case_key: 'something' }); + }); + it('returns empty object when object is empty', () => { + const values = {}; + expect(convertToSnakeCase(values)).toEqual({}); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 55832ab67dc6b..36e1a814d8ec2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -5,6 +5,7 @@ */ import Boom from 'boom'; +import { snakeCase } from 'lodash/fp'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; import { LegacyServices } from '../../../types'; @@ -211,3 +212,11 @@ export const getIndex = (getSpaceId: () => string, config: LegacyServices['confi return `${signalsIndex}-${spaceId}`; }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const convertToSnakeCase = >(obj: T): Partial | null => { + return Object.keys(obj).reduce((acc, item) => { + const newKey = snakeCase(item); + return { ...acc, [newKey]: obj[item] }; + }, {}); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index d4b7c252e3e38..b1dc62f6fc90f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -5,12 +5,10 @@ */ import { Readable } from 'stream'; import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson'; -import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams'; +import { createPromiseFromStreams } from 'src/legacy/utils/streams'; import { ImportRuleAlertRest } from '../types'; -const readStreamToCompletion = (stream: Readable) => { - return createPromiseFromStreams([stream, createConcatStream([])]); -}; +type PromiseFromStreams = ImportRuleAlertRest | Error; export const getOutputSample = (): Partial => ({ rule_id: 'rule-1', @@ -43,8 +41,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); expect(result).toEqual([ { rule_id: 'rule-1', @@ -95,6 +96,22 @@ describe('create_rules_stream_from_ndjson', () => { ]); }); + test('returns error when ndjson stream is larger than limit', async () => { + const sample1 = getOutputSample(); + const sample2 = getOutputSample(); + sample2.rule_id = 'rule-2'; + const ndJsonStream = new Readable({ + read() { + this.push(getSampleAsNdjson(sample1)); + this.push(getSampleAsNdjson(sample2)); + }, + }); + const rulesObjectsStream = createRulesStreamFromNdJson(1); + await expect( + createPromiseFromStreams([ndJsonStream, ...rulesObjectsStream]) + ).rejects.toThrowError("Can't import more than 1 rules"); + }); + test('skips empty lines', async () => { const sample1 = getOutputSample(); const sample2 = getOutputSample(); @@ -108,8 +125,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); expect(result).toEqual([ { rule_id: 'rule-1', @@ -172,8 +192,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); expect(result).toEqual([ { rule_id: 'rule-1', @@ -236,8 +259,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); const resultOrError = result as Error[]; expect(resultOrError[0]).toEqual({ rule_id: 'rule-1', @@ -300,8 +326,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); const resultOrError = result as TypeError[]; expect(resultOrError[0]).toEqual({ rule_id: 'rule-1', @@ -366,8 +395,11 @@ describe('create_rules_stream_from_ndjson', () => { this.push(null); }, }); - const rulesObjectsStream = createRulesStreamFromNdJson(ndJsonStream, 1000); - const result = await readStreamToCompletion(rulesObjectsStream); + const rulesObjectsStream = createRulesStreamFromNdJson(1000); + const result = await createPromiseFromStreams([ + ndJsonStream, + ...rulesObjectsStream, + ]); const resultOrError = result as TypeError[]; expect(resultOrError[1] instanceof TypeError).toEqual(true); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index 6d58171a3245d..ae0dfa20852aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Readable, Transform } from 'stream'; +import { Transform } from 'stream'; import { has, isString } from 'lodash/fp'; import { ImportRuleAlertRest } from '../types'; import { @@ -74,15 +74,13 @@ export const createLimitStream = (limit: number): Transform => { * Inspiration and the pattern of code followed is from: * saved_objects/lib/create_saved_objects_stream_from_ndjson.ts */ -export const createRulesStreamFromNdJson = ( - ndJsonStream: Readable, - ruleLimit: number -): Transform => { - return ndJsonStream - .pipe(createSplitStream('\n')) - .pipe(parseNdjsonStrings()) - .pipe(filterExportedCounts()) - .pipe(validateRules()) - .pipe(createLimitStream(ruleLimit)) - .pipe(createConcatStream([])); +export const createRulesStreamFromNdJson = (ruleLimit: number) => { + return [ + createSplitStream('\n'), + parseNdjsonStrings(), + filterExportedCounts(), + validateRules(), + createLimitStream(ruleLimit), + createConcatStream([]), + ]; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 98f5df4852530..44d3013263c65 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -10,9 +10,15 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; +import * as readRules from './read_rules'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; describe('get_export_by_object_ids', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); describe('getExportByObjectIds', () => { test('it exports object ids into an expected string with new line characters', async () => { const alertsClient = alertsClientMock.create(); @@ -119,6 +125,23 @@ describe('get_export_by_object_ids', () => { expect(exports).toEqual(expected); }); + test('it returns error when readRules throws error', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + jest.spyOn(readRules, 'readRules').mockImplementation(async () => { + throw new Error('Test error'); + }); + const objects = [{ rule_id: 'rule-1' }]; + const exports = await getRulesFromObjects(alertsClient, objects); + const expected: RulesErrors = { + exportedCount: 0, + missingRules: [{ rule_id: objects[0].rule_id }], + rules: [], + }; + expect(exports).toEqual(expected); + }); + test('it does not transform the rule if the rule is an immutable rule and designates it as a missing rule', async () => { const alertsClient = alertsClientMock.create(); const result = getResult(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 45507a69f50c2..aa1cce6f15238 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -8,7 +8,23 @@ import { readRules } from './read_rules'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; +class TestError extends Error { + constructor() { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(); + + this.name = 'CustomError'; + this.output = { statusCode: 404 }; + } + public output: { statusCode: number }; +} + describe('read_rules', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); describe('readRules', () => { test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { const alertsClient = alertsClientMock.create(); @@ -21,6 +37,49 @@ describe('read_rules', () => { }); expect(rule).toEqual(getResult()); }); + test('should return null if saved object found by alerts client given id is not alert type', async () => { + const alertsClient = alertsClientMock.create(); + const { alertTypeId, ...rest } = getResult(); + // @ts-ignore + alertsClient.get.mockImplementation(() => rest); + + const rule = await readRules({ + alertsClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + expect(rule).toEqual(null); + }); + + test('should return error if alerts client throws 404 error on get', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockImplementation(() => { + throw new TestError(); + }); + + const rule = await readRules({ + alertsClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + expect(rule).toEqual(null); + }); + + test('should return error if alerts client throws error on get', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockImplementation(() => { + throw new Error('Test error'); + }); + try { + await readRules({ + alertsClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + } catch (exc) { + expect(exc.message).toEqual('Test error'); + } + }); test('should return the output from alertsClient if id is set but ruleId is null', async () => { const alertsClient = alertsClientMock.create(); @@ -47,6 +106,20 @@ describe('read_rules', () => { expect(rule).toEqual(getResult()); }); + test('should return null if the output from alertsClient with ruleId set is empty', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + // @ts-ignore + alertsClient.find.mockResolvedValue({ data: [] }); + + const rule = await readRules({ + alertsClient, + id: undefined, + ruleId: 'rule-1', + }); + expect(rule).toEqual(null); + }); + test('should return the output from alertsClient if id is null but ruleId is set', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index cbe6dbda8449f..94e4e6357a4a0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -31,7 +31,7 @@ export const readRules = async ({ return null; } } catch (err) { - if (err.output.statusCode === 404) { + if (err?.output?.statusCode === 404) { return null; } else { // throw non-404 as they would be 500 or other internal errors diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index fa22765c143e1..3d95e9868a1d6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -197,10 +197,6 @@ export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; -export const isRuleStatusAttributes = (obj: unknown): obj is IRuleStatusAttributes => { - return get('lastSuccessMessage', obj) != null; -}; - export const isRuleStatusSavedObjectType = ( obj: unknown ): obj is SavedObject => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh index b4a494a102b54..ec3e0595c7b08 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signal_index_exists.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./signal_index_exists.sh -curl -s -k --head \ +curl -s -k -f \ -H 'Content-Type: application/json' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index + ${KIBANA_URL}${SPACE_URL}/api/detection_engine/index > /dev/null diff --git a/x-pack/legacy/plugins/upgrade_assistant/index.ts b/x-pack/legacy/plugins/upgrade_assistant/index.ts index 3f98ff60a91ce..b5e8ce4750215 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/index.ts @@ -3,25 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; -import Joi from 'joi'; import { Legacy } from 'kibana'; -import { resolve } from 'path'; import mappings from './mappings.json'; -import { plugin } from './server/np_ready'; -import { CloudSetup } from '../../../plugins/cloud/server'; export function upgradeAssistant(kibana: any) { - const publicSrc = resolve(__dirname, 'public'); - const npSrc = resolve(publicSrc, 'np_ready'); - const config: Legacy.PluginSpecOptions = { id: 'upgrade_assistant', - configPrefix: 'xpack.upgrade_assistant', - require: ['elasticsearch'], uiExports: { // @ts-ignore - managementSections: ['plugins/upgrade_assistant'], savedObjectSchemas: { 'upgrade-assistant-reindex-operation': { isNamespaceAgnostic: true, @@ -30,41 +19,10 @@ export function upgradeAssistant(kibana: any) { isNamespaceAgnostic: true, }, }, - styleSheetPaths: resolve(npSrc, 'application/index.scss'), mappings, }, - publicDir: publicSrc, - - config() { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server: Legacy.Server) { - // Add server routes and initialize the plugin here - const instance = plugin({} as any); - const { usageCollection, cloud } = server.newPlatform.setup.plugins; - instance.setup(server.newPlatform.setup.core, { - usageCollection, - cloud: cloud as CloudSetup, - __LEGACY: { - // Legacy objects - events: server.events, - savedObjects: server.savedObjects, - - // Legacy functions - log: server.log.bind(server), - - // Legacy plugins - plugins: { - elasticsearch: server.plugins.elasticsearch, - xpack_main: server.plugins.xpack_main, - }, - }, - }); - }, + init() {}, }; return new kibana.Plugin(config); } diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts b/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts deleted file mode 100644 index b6bc6a14de224..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/legacy.ts +++ /dev/null @@ -1,106 +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 { ComponentType } from 'react'; -import { i18n } from '@kbn/i18n'; - -/* LEGACY IMPORTS */ -import { npSetup } from 'ui/new_platform'; -import { wrapInI18nContext } from 'ui/i18n'; -import { management } from 'ui/management'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import routes from 'ui/routes'; -import chrome from 'ui/chrome'; -/* LEGACY IMPORTS */ - -import { NEXT_MAJOR_VERSION } from '../common/version'; -import { plugin } from './np_ready'; -import { CloudSetup } from '../../../../plugins/cloud/public'; - -const BASE_PATH = `/management/elasticsearch/upgrade_assistant`; - -export interface LegacyAppMountParameters { - __LEGACY: { renderToElement: (RootComponent: ComponentType) => void }; -} - -export interface LegacyApp { - mount(ctx: any, params: LegacyAppMountParameters): void; -} - -export interface LegacyManagementPlugin { - sections: { - get( - name: string - ): { - registerApp(app: LegacyApp): void; - }; - }; -} - -// Based on /rfcs/text/0006_management_section_service.md -export interface LegacyPlugins { - cloud?: CloudSetup; - management: LegacyManagementPlugin; - __LEGACY: { - XSRF: string; - }; -} - -function startApp() { - routes.when(`${BASE_PATH}/:view?`, { - template: - '', - }); - const { cloud } = npSetup.plugins as any; - const legacyPluginsShim: LegacyPlugins = { - cloud: cloud as CloudSetup, - __LEGACY: { - XSRF: chrome.getXsrfToken(), - }, - management: { - sections: { - get(_: string) { - return { - registerApp(app) { - management.getSection('elasticsearch').register('upgrade_assistant', { - visible: true, - display: i18n.translate('xpack.upgradeAssistant.appTitle', { - defaultMessage: '{version} Upgrade Assistant', - values: { version: `${NEXT_MAJOR_VERSION}.0` }, - }), - order: 100, - url: `#${BASE_PATH}`, - }); - - app.mount( - {}, - { - __LEGACY: { - // While there is not an NP API for registering management section apps yet - renderToElement: RootComponent => { - uiModules - .get('kibana') - .directive('upgradeAssistant', (reactDirective: any) => { - return reactDirective(wrapInI18nContext(RootComponent)); - }); - }, - }, - } - ); - }, - }; - }, - }, - }, - }; - - const pluginInstance = plugin(); - - pluginInstance.setup(npSetup.core, legacyPluginsShim); -} - -startApp(); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx deleted file mode 100644 index 571967ab114c9..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { EuiPageHeader, EuiPageHeaderSection, EuiTitle } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { NEXT_MAJOR_VERSION } from '../../../common/version'; -import { UpgradeAssistantTabs } from './components/tabs'; -import { AppContextProvider, ContextValue, AppContext } from './app_context'; - -type AppDependencies = ContextValue; - -export const RootComponent = (deps: AppDependencies) => { - return ( - -
- - - -

- -

-
-
-
- - {({ http }) => } - -
-
- ); -}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts deleted file mode 100644 index dc7a758839fe5..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.mocks.ts +++ /dev/null @@ -1,24 +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 { ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; - -export const mockClient = { - post: jest.fn().mockResolvedValue({ - lastCompletedStep: ReindexStep.created, - status: ReindexStatus.inProgress, - }), - get: jest.fn().mockResolvedValue({ - status: 200, - data: { - warnings: [], - reindexOp: null, - }, - }), -}; -jest.mock('axios', () => ({ - create: jest.fn().mockReturnValue(mockClient), -})); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts deleted file mode 100644 index ed85b988c25d6..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/plugin.ts +++ /dev/null @@ -1,24 +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 { Plugin, CoreSetup } from 'src/core/public'; -import { RootComponent } from './application/app'; -import { LegacyPlugins } from '../legacy'; - -export class UpgradeAssistantUIPlugin implements Plugin { - async setup({ http }: CoreSetup, { cloud, management, __LEGACY: { XSRF } }: LegacyPlugins) { - const appRegistrar = management.sections.get('kibana'); - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - - return appRegistrar.registerApp({ - mount(__, { __LEGACY: { renderToElement } }) { - return renderToElement(() => RootComponent({ http, XSRF, isCloudEnabled })); - }, - }); - } - async start() {} - async stop() {} -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts deleted file mode 100644 index b52b3b812b7f9..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.ts +++ /dev/null @@ -1,47 +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 { - UIOpen, - UIOpenOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../../common/types'; -import { RequestShim, ServerShim } from '../../types'; - -async function incrementUIOpenOptionCounter(server: ServerShim, uiOpenOptionCounter: UIOpenOption) { - const { getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_open.${uiOpenOptionCounter}` - ); -} - -export async function upsertUIOpenOption(server: ServerShim, req: RequestShim): Promise { - const { overview, cluster, indices } = req.payload as UIOpen; - - if (overview) { - await incrementUIOpenOptionCounter(server, 'overview'); - } - - if (cluster) { - await incrementUIOpenOptionCounter(server, 'cluster'); - } - - if (indices) { - await incrementUIOpenOptionCounter(server, 'indices'); - } - - return { - overview, - cluster, - indices, - }; -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts deleted file mode 100644 index 626d51b298e72..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.ts +++ /dev/null @@ -1,58 +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 { - UIReindex, - UIReindexOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../../common/types'; -import { RequestShim, ServerShim } from '../../types'; - -async function incrementUIReindexOptionCounter( - server: ServerShim, - uiOpenOptionCounter: UIReindexOption -) { - const { getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.${uiOpenOptionCounter}` - ); -} - -export async function upsertUIReindexOption( - server: ServerShim, - req: RequestShim -): Promise { - const { close, open, start, stop } = req.payload as UIReindex; - - if (close) { - await incrementUIReindexOptionCounter(server, 'close'); - } - - if (open) { - await incrementUIReindexOptionCounter(server, 'open'); - } - - if (start) { - await incrementUIReindexOptionCounter(server, 'start'); - } - - if (stop) { - await incrementUIReindexOptionCounter(server, 'stop'); - } - - return { - close, - open, - start, - stop, - }; -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts deleted file mode 100644 index fae369fa59394..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts +++ /dev/null @@ -1,49 +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 { Plugin, CoreSetup, CoreStart } from 'src/core/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ServerShim, ServerShimWithRouter } from './types'; -import { credentialStoreFactory } from './lib/reindexing/credential_store'; -import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; -import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; -import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; -import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; -import { CloudSetup } from '../../../../../plugins/cloud/server'; -import { registerTelemetryRoutes } from './routes/telemetry'; - -interface PluginsSetup { - __LEGACY: ServerShim; - usageCollection: UsageCollectionSetup; - cloud?: CloudSetup; -} - -export class UpgradeAssistantServerPlugin implements Plugin { - setup({ http }: CoreSetup, { __LEGACY, usageCollection, cloud }: PluginsSetup) { - const router = http.createRouter(); - const shimWithRouter: ServerShimWithRouter = { ...__LEGACY, router }; - registerClusterCheckupRoutes(shimWithRouter, { cloud }); - registerDeprecationLoggingRoutes(shimWithRouter); - - // The ReindexWorker uses a map of request headers that contain the authentication credentials - // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not - // want to expose these credentials to any unauthenticated users. We also want to avoid any need - // to add a user for a special index just for upgrading. This in-memory cache allows us to - // process jobs without the browser staying on the page, but will require that jobs go into - // a paused state if no Kibana nodes have the required credentials. - const credentialStore = credentialStoreFactory(); - - const worker = registerReindexWorker(__LEGACY, credentialStore); - registerReindexIndicesRoutes(shimWithRouter, worker, credentialStore); - - // Bootstrap the needed routes and the collector for the telemetry - registerTelemetryRoutes(shimWithRouter); - registerUpgradeAssistantUsageCollector(usageCollection, __LEGACY); - } - - start(core: CoreStart, plugins: any) {} - - stop(): void {} -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts deleted file mode 100644 index 81cf690d813ad..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.ts +++ /dev/null @@ -1,46 +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 _ from 'lodash'; -import { ServerShimWithRouter } from '../types'; -import { getUpgradeAssistantStatus } from '../lib/es_migration_apis'; -import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { CloudSetup } from '../../../../../../plugins/cloud/server'; -import { createRequestShim } from './create_request_shim'; - -interface PluginsSetup { - cloud?: CloudSetup; -} - -export function registerClusterCheckupRoutes( - server: ServerShimWithRouter, - pluginsSetup: PluginsSetup -) { - const { cloud } = pluginsSetup; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - - server.router.get( - { - path: '/api/upgrade_assistant/status', - validate: false, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - try { - return response.ok({ - body: await getUpgradeAssistantStatus(callWithRequest, reqShim, isCloudEnabled), - }); - } catch (e) { - if (e.status === 403) { - return response.forbidden(e.message); - } - - return response.internalError({ body: e }); - } - }) - ); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts deleted file mode 100644 index b1a5c8b72d0e0..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/create_request_shim.ts +++ /dev/null @@ -1,16 +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 { KibanaRequest } from 'kibana/server'; -import { RequestShim } from '../types'; - -export const createRequestShim = (req: KibanaRequest): RequestShim => { - return { - headers: req.headers as Record, - payload: req.body || (req as any).payload, - params: req.params, - }; -}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts deleted file mode 100644 index 7e19ef3fb6047..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.ts +++ /dev/null @@ -1,57 +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 { schema } from '@kbn/config-schema'; - -import { - getDeprecationLoggingStatus, - setDeprecationLogging, -} from '../lib/es_deprecation_logging_apis'; -import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { ServerShimWithRouter } from '../types'; -import { createRequestShim } from './create_request_shim'; - -export function registerDeprecationLoggingRoutes(server: ServerShimWithRouter) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - - server.router.get( - { - path: '/api/upgrade_assistant/deprecation_logging', - validate: false, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - try { - const result = await getDeprecationLoggingStatus(callWithRequest, reqShim); - return response.ok({ body: result }); - } catch (e) { - return response.internalError({ body: e }); - } - }) - ); - - server.router.put( - { - path: '/api/upgrade_assistant/deprecation_logging', - validate: { - body: schema.object({ - isEnabled: schema.boolean(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - try { - const { isEnabled } = reqShim.payload as { isEnabled: boolean }; - return response.ok({ - body: await setDeprecationLogging(callWithRequest, reqShim, isEnabled), - }); - } catch (e) { - return response.internalError({ body: e }); - } - }) - ); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts deleted file mode 100644 index c22f12316bd02..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.ts +++ /dev/null @@ -1,207 +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 { schema } from '@kbn/config-schema'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsClientContract } from 'kibana/server'; -import { ReindexStatus } from '../../../common/types'; -import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; -import { CredentialStore } from '../lib/reindexing/credential_store'; -import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; -import { ServerShim, ServerShimWithRouter } from '../types'; -import { createRequestShim } from './create_request_shim'; - -export function registerReindexWorker(server: ServerShim, credentialStore: CredentialStore) { - const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster( - 'admin' - ); - const xpackInfo = server.plugins.xpack_main.info; - const savedObjectsRepository = server.savedObjects.getSavedObjectsRepository( - callWithInternalUser - ); - const savedObjectsClient = new server.savedObjects.SavedObjectsClient( - savedObjectsRepository - ) as SavedObjectsClientContract; - - // Cannot pass server.log directly because it's value changes during startup (?). - // Use this function to proxy through. - const log = (tags: string | string[], data?: string | object | (() => any), timestamp?: number) => - server.log(tags, data, timestamp); - - const worker = new ReindexWorker( - savedObjectsClient, - credentialStore, - callWithRequest, - callWithInternalUser, - xpackInfo, - log - ); - - // Wait for ES connection before starting the polling loop. - server.plugins.elasticsearch.waitUntilReady().then(() => { - worker.start(); - server.events.on('stop', () => worker.stop()); - }); - - return worker; -} - -export function registerReindexIndicesRoutes( - server: ServerShimWithRouter, - worker: ReindexWorker, - credentialStore: CredentialStore -) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const xpackInfo = server.plugins.xpack_main.info; - const BASE_PATH = '/api/upgrade_assistant/reindex'; - - // Start reindex for an index - server.router.post( - { - path: `${BASE_PATH}/{indexName}`, - validate: { - params: schema.object({ - indexName: schema.string(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - const { indexName } = reqShim.params; - const { client } = ctx.core.savedObjects; - const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - if (!(await reindexService.hasRequiredPrivileges(indexName))) { - return response.forbidden({ - body: `You do not have adequate privileges to reindex this index.`, - }); - } - - const existingOp = await reindexService.findReindexOperation(indexName); - - // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. - const reindexOp = - existingOp && existingOp.attributes.status === ReindexStatus.paused - ? await reindexService.resumeReindexOperation(indexName) - : await reindexService.createReindexOperation(indexName); - - // Add users credentials for the worker to use - credentialStore.set(reindexOp, reqShim.headers); - - // Kick the worker on this node to immediately pickup the new reindex operation. - worker.forceRefresh(); - - return response.ok({ body: reindexOp.attributes }); - } catch (e) { - return response.internalError({ body: e }); - } - }) - ); - - // Get status - server.router.get( - { - path: `${BASE_PATH}/{indexName}`, - validate: { - params: schema.object({ - indexName: schema.string(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - const { client } = ctx.core.savedObjects; - const { indexName } = reqShim.params; - const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName); - const reindexOp = await reindexService.findReindexOperation(indexName); - // If the user doesn't have privileges than querying for warnings is going to fail. - const warnings = hasRequiredPrivileges - ? await reindexService.detectReindexWarnings(indexName) - : []; - const indexGroup = reindexService.getIndexGroup(indexName); - - return response.ok({ - body: { - reindexOp: reindexOp ? reindexOp.attributes : null, - warnings, - indexGroup, - hasRequiredPrivileges, - }, - }); - } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); - } - }) - ); - - // Cancel reindex - server.router.post( - { - path: `${BASE_PATH}/{indexName}/cancel`, - validate: { - params: schema.object({ - indexName: schema.string(), - }), - }, - }, - versionCheckHandlerWrapper(async (ctx, request, response) => { - const reqShim = createRequestShim(request); - const { indexName } = reqShim.params; - const { client } = ctx.core.savedObjects; - const callCluster = callWithRequest.bind(null, reqShim) as CallCluster; - const reindexActions = reindexActionsFactory(client, callCluster); - const reindexService = reindexServiceFactory( - callCluster, - xpackInfo, - reindexActions, - server.log - ); - - try { - await reindexService.cancelReindexing(indexName); - - return response.ok({ body: { acknowledged: true } }); - } catch (e) { - if (!e.isBoom) { - return response.internalError({ body: e }); - } - return response.customError({ - body: { - message: e.message, - }, - statusCode: e.statusCode, - }); - } - }) - ); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts deleted file mode 100644 index 77ba97529c32f..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts +++ /dev/null @@ -1,29 +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 { Legacy } from 'kibana'; -import { IRouter } from 'src/core/server'; -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; - -export interface ServerShim { - plugins: { - elasticsearch: ElasticsearchPlugin; - xpack_main: XPackMainPlugin; - }; - log: any; - events: any; - savedObjects: Legacy.SavedObjectsService; -} - -export interface ServerShimWithRouter extends ServerShim { - router: IRouter; -} - -export interface RequestShim { - headers: Record; - payload: any; - params: any; -} diff --git a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts index f6fa569a50315..00781726941d5 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + export const PLUGIN = { APP_ROOT_ID: 'react-uptime-root', DESCRIPTION: 'Uptime monitoring', ID: 'uptime', - ROUTER_BASE_NAME: '/app/uptime#', LOCAL_STORAGE_KEY: 'xpack.uptime', + NAME: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { + defaultMessage: 'Uptime', + }), + ROUTER_BASE_NAME: '/app/uptime#', TITLE: 'uptime', }; diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index cf7332f97d466..feecef5857895 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -6,9 +6,7 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; -import { PluginInitializerContext } from 'src/core/server'; import { PLUGIN } from './common/constants'; -import { KibanaServer, plugin } from './server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export const uptime = (kibana: any) => @@ -35,21 +33,4 @@ export const uptime = (kibana: any) => }, home: ['plugins/uptime/register_feature'], }, - init(server: KibanaServer) { - const initializerContext = {} as PluginInitializerContext; - const { savedObjects } = server; - const { xpack_main } = server.plugins; - const { usageCollection } = server.newPlatform.setup.plugins; - - plugin(initializerContext).setup( - { - route: server.newPlatform.setup.core.http.createRouter(), - }, - { - savedObjects, - usageCollection, - xpack: xpack_main, - } - ); - }, }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap index ebbd8a4ac56a8..db41dfb0b04c4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/snapshot.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Snapshot component renders without errors 1`] = ` loading={false} >

- All monitors are up + 23 + + Monitors

`; @@ -15,7 +17,9 @@ exports[`SnapshotHeading renders custom heading for no monitors 1`] = ` size="s" >

- No monitors found + 0 + + Monitors

`; @@ -25,7 +29,9 @@ exports[`SnapshotHeading renders standard heading for valid counts 1`] = ` size="s" >

- 3/17 monitors are down + 17 + + Monitors

`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx index 5ddef3d0aabd1..70d082b26d653 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot_heading.test.tsx @@ -10,17 +10,17 @@ import { SnapshotHeading } from '../snapshot_heading'; describe('SnapshotHeading', () => { it('renders custom heading for no down monitors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); it('renders standard heading for valid counts', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); it('renders custom heading for no monitors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap index c1b5970f6456c..71690432fd01b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap @@ -114,7 +114,6 @@ exports[`ChartWrapper component renders the component with loading false 1`] = ` } > { it('renders the component with loading false', () => { const component = shallowWithIntl( - + @@ -29,7 +29,7 @@ describe('ChartWrapper component', () => { it('renders the component with loading true', () => { const component = shallowWithIntl( - + @@ -40,7 +40,7 @@ describe('ChartWrapper component', () => { it('mounts the component with loading true or false', async () => { const component = mount( - + @@ -62,7 +62,7 @@ describe('ChartWrapper component', () => { it('mounts the component with chart when loading true or false', async () => { const component = mount( - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index 8531cd1a3cc83..999ade9dccdd9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -6,7 +6,6 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; -import { get } from 'lodash'; import { DonutChart } from './charts'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; @@ -28,11 +27,11 @@ interface SnapshotComponentProps { */ export const SnapshotComponent: React.FC = ({ count, height, loading }) => ( - (count, 'down', 0)} total={get(count, 'total', 0)} /> + (count, 'up', 0)} - down={get(count, 'down', 0)} + up={count.up} + down={count.down} height={SNAPSHOT_CHART_HEIGHT} width={SNAPSHOT_CHART_WIDTH} /> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx index 85d1294d4b064..308d6e19241c2 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot_heading.tsx @@ -8,32 +8,17 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -interface Props { - down: number; - total: number; -} +export const SnapshotHeading = ({ total }: { total: number }) => { + const monitorsText = + total === 1 + ? i18n.translate('xpack.uptime.snapshot.monitor', { defaultMessage: 'Monitor' }) + : i18n.translate('xpack.uptime.snapshot.monitors', { defaultMessage: 'Monitors' }); -const getMessage = (down: number, total: number): string => { - if (down === 0 && total > 0) { - return i18n.translate('xpack.uptime.snapshot.zeroDownMessage', { - defaultMessage: 'All monitors are up', - }); - } else if (down === 0 && total === 0) { - return i18n.translate('xpack.uptime.snapshot.noMonitorMessage', { - defaultMessage: 'No monitors found', - }); - } - return i18n.translate('xpack.uptime.snapshot.downCountsMessage', { - defaultMessage: '{down}/{total} monitors are down', - values: { - down, - total, - }, - }); + return ( + +

+ {total} {monitorsText} +

+
+ ); }; - -export const SnapshotHeading = ({ down, total }: Props) => ( - -

{getMessage(down, total)}

-
-); diff --git a/x-pack/legacy/plugins/uptime/scripts/graphql_schemas.ts b/x-pack/legacy/plugins/uptime/scripts/graphql_schemas.ts deleted file mode 100644 index c337cf098e48d..0000000000000 --- a/x-pack/legacy/plugins/uptime/scripts/graphql_schemas.ts +++ /dev/null @@ -1,15 +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 { buildSchemaFromTypeDefinitions } from 'graphql-tools'; -import { typeDefs } from '../server/graphql'; - -export const schemas = [...typeDefs]; - -// this default export is used to feed the combined types to the gql-gen tool -// which generates the corresponding typescript types -// eslint-disable-next-line import/no-default-export -export default buildSchemaFromTypeDefinitions(schemas); diff --git a/x-pack/legacy/plugins/uptime/server/plugin.ts b/x-pack/legacy/plugins/uptime/server/plugin.ts deleted file mode 100644 index acecce305e7cb..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/plugin.ts +++ /dev/null @@ -1,19 +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 { PluginInitializerContext } from 'src/core/server'; -import { initServerWithKibana } from './kibana.index'; -import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters/framework'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(); -} - -export class Plugin { - public setup(core: UptimeCoreSetup, plugins: UptimeCorePlugins) { - initServerWithKibana(core, plugins); - } -} diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap new file mode 100644 index 0000000000000..1a70504dc9391 --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Telemetry Collection: Get Aggregated Stats OSS-like telemetry (no license nor X-Pack telemetry) 1`] = ` +Array [ + Object { + "cluster_name": "test", + "cluster_stats": Object {}, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", + }, + ], + "platforms": Array [ + Object { + "count": 1, + "platform": "rocky", + }, + ], + }, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], + }, + }, + "version": "8.0.0", + }, +] +`; + +exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry (license + X-Pack) 1`] = ` +Array [ + Object { + "cluster_name": "test", + "cluster_stats": Object {}, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", + }, + ], + "platforms": Array [ + Object { + "count": 1, + "platform": "rocky", + }, + ], + }, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], + }, + "xpack": Object {}, + }, + "version": "8.0.0", + }, +] +`; diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js index eca130b4d7465..eb03701fd195b 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js @@ -8,42 +8,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { TIMEOUT } from '../constants'; -import { getXPackLicense, getXPackUsage, getXPack, handleXPack } from '../get_xpack'; - -function mockGetXPackLicense(callCluster, license, req) { - callCluster - .withArgs(req, 'transport.request', { - method: 'GET', - path: '/_license', - query: { - local: 'true', - accept_enterprise: 'true', - }, - }) - .returns( - license.then( - response => ({ license: response }), - () => {} // Catch error so that we don't emit UnhandledPromiseRejectionWarning for tests with invalid license - ) - ); - - callCluster - .withArgs('transport.request', { - method: 'GET', - path: '/_license', - query: { - local: 'true', - accept_enterprise: 'true', - }, - }) - // conveniently wraps the passed in license object as { license: response }, like it really is - .returns( - license.then( - response => ({ license: response }), - () => {} // Catch error so that we don't emit UnhandledPromiseRejectionWarning for tests with invalid license - ) - ); -} +import { getXPackUsage } from '../get_xpack'; function mockGetXPackUsage(callCluster, usage, req) { callCluster @@ -67,31 +32,7 @@ function mockGetXPackUsage(callCluster, usage, req) { .returns(usage); } -/** - * Mock getXPack responses. - * - * @param {Function} callCluster Sinon function mock. - * @param {Promise} license Promised license response. - * @param {Promise} usage Promised usage response. - * @param {Object} usage reqeust object. - */ -export function mockGetXPack(callCluster, license, usage, req) { - mockGetXPackLicense(callCluster, license, req); - mockGetXPackUsage(callCluster, usage, req); -} - describe('get_xpack', () => { - describe('getXPackLicense', () => { - it('uses callCluster to get /_license API', async () => { - const response = { type: 'basic' }; - const callCluster = sinon.stub(); - - mockGetXPackLicense(callCluster, Promise.resolve(response)); - - expect(await getXPackLicense(callCluster)).to.eql(response); - }); - }); - describe('getXPackUsage', () => { it('uses callCluster to get /_xpack/usage API', () => { const response = Promise.resolve({}); @@ -102,48 +43,4 @@ describe('get_xpack', () => { expect(getXPackUsage(callCluster)).to.be(response); }); }); - - describe('handleXPack', () => { - it('uses data as expected', () => { - const license = { fake: 'data' }; - const usage = { also: 'fake', nested: { object: { data: [{ field: 1 }, { field: 2 }] } } }; - - expect(handleXPack(license, usage)).to.eql({ license, stack_stats: { xpack: usage } }); - }); - }); - - describe('getXPack', () => { - it('returns the formatted response object', async () => { - const license = { fancy: 'license' }; - const xpack = { also: 'fancy' }; - - const callCluster = sinon.stub(); - - mockGetXPack(callCluster, Promise.resolve(license), Promise.resolve(xpack)); - - const data = await getXPack(callCluster); - - expect(data).to.eql({ license, xpack }); - }); - - it('returns empty object upon license failure', async () => { - const callCluster = sinon.stub(); - - mockGetXPack(callCluster, Promise.reject(new Error()), Promise.resolve({ also: 'fancy' })); - - const data = await getXPack(callCluster); - - expect(data).to.eql({}); - }); - - it('returns empty object upon usage failure', async () => { - const callCluster = sinon.stub(); - - mockGetXPack(callCluster, Promise.resolve({ fancy: 'license' }), Promise.reject(new Error())); - - const data = await getXPack(callCluster); - - expect(data).to.eql({}); - }); - }); }); diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts index c89fbe416a0cc..b6f1aabab95c4 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts @@ -7,4 +7,5 @@ /** * The timeout used by each request, whenever a timeout can be specified. */ + export const TIMEOUT = '30s'; diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.test.ts new file mode 100644 index 0000000000000..b85cbd9661022 --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { getStatsWithXpack } from './get_stats_with_xpack'; + +const kibana = { + kibana: { + great: 'googlymoogly', + versions: [{ version: '8675309', count: 1 }], + }, + kibana_stats: { + os: { + platform: 'rocky', + platformRelease: 'iv', + }, + }, + localization: { + locale: 'en', + labelsCount: 0, + integrities: {}, + }, + sun: { chances: 5 }, + clouds: { chances: 95 }, + rain: { chances: 2 }, + snow: { chances: 0 }, +}; + +const getMockServer = (getCluster = jest.fn()) => ({ + log(tags: string[], message: string) { + // eslint-disable-next-line no-console + console.log({ tags, message }); + }, + config() { + return { + get(item: string) { + switch (item) { + case 'pkg.version': + return '8675309-snapshot'; + default: + throw Error(`unexpected config.get('${item}') received.`); + } + }, + }; + }, + plugins: { + elasticsearch: { getCluster }, + }, +}); + +const mockUsageCollection = (kibanaUsage = kibana) => ({ + bulkFetch: () => kibanaUsage, + toObject: (data: any) => data, +}); + +describe('Telemetry Collection: Get Aggregated Stats', () => { + test('OSS-like telemetry (no license nor X-Pack telemetry)', async () => { + const callCluster = jest.fn(async (method: string, options: { path?: string }) => { + switch (method) { + case 'transport.request': + if (options.path === '/_license' || options.path === '/_xpack/usage') { + // eslint-disable-next-line no-throw-literal + throw { statusCode: 404 }; + } + return {}; + case 'info': + return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; + default: + return {}; + } + }); + const usageCollection = mockUsageCollection(); + const server = getMockServer(); + + const stats = await getStatsWithXpack([{ clusterUuid: '1234' }], { + callCluster, + usageCollection, + server, + } as any); + expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + }); + + test('X-Pack telemetry (license + X-Pack)', async () => { + const callCluster = jest.fn(async (method: string, options: { path?: string }) => { + switch (method) { + case 'transport.request': + if (options.path === '/_license') { + return { + license: { type: 'basic' }, + }; + } + if (options.path === '/_xpack/usage') { + return {}; + } + case 'info': + return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; + default: + return {}; + } + }); + const usageCollection = mockUsageCollection(); + const server = getMockServer(); + + const stats = await getStatsWithXpack([{ clusterUuid: '1234' }], { + callCluster, + usageCollection, + server, + } as any); + expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts index 41076d96231c9..ea7465f66f120 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts @@ -4,19 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import { getXPack } from './get_xpack'; -import { getLocalStats } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection'; import { StatsGetter } from '../../../../../../src/legacy/core_plugins/telemetry/server/collection_manager'; +import { + getLocalStats, + TelemetryLocalStats, +} from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats'; +import { getXPackUsage } from './get_xpack'; -export const getStatsWithXpack: StatsGetter = async function(clustersDetails, config) { +export type TelemetryAggregatedStats = TelemetryLocalStats & { + stack_stats: { xpack?: object }; +}; + +export const getStatsWithXpack: StatsGetter = async function( + clustersDetails, + config +) { const { callCluster } = config; const clustersLocalStats = await getLocalStats(clustersDetails, config); - const { license, xpack } = await getXPack(callCluster); + const xpack = await getXPackUsage(callCluster).catch(() => undefined); // We want to still report something (and do not lose the license) even when this method fails. return clustersLocalStats.map(localStats => { - localStats.license = license; - localStats.stack_stats.xpack = xpack; + if (xpack) { + return { + ...localStats, + stack_stats: { ...localStats.stack_stats, xpack }, + }; + } + return localStats; }); }; diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js deleted file mode 100644 index aaeb890981aa1..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js +++ /dev/null @@ -1,85 +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 { TIMEOUT } from './constants'; - -/** - * Get the cluster stats from the connected cluster. - * - * This is the equivalent of GET /_license?local=true . - * - * Like any X-Pack related API, X-Pack must installed for this to work. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch. - */ -export function getXPackLicense(callCluster) { - return callCluster('transport.request', { - method: 'GET', - path: '/_license', - query: { - // Fetching the local license is cheaper than getting it from the master and good enough - local: 'true', - // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. - accept_enterprise: 'true', - }, - }).then(({ license }) => license); -} - -/** - * Get the cluster stats from the connected cluster. - * - * This is the equivalent of GET /_xpack/usage?master_timeout=${TIMEOUT} - * - * Like any X-Pack related API, X-Pack must installed for this to work. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch equivalent to GET /_cluster/stats. - */ -export function getXPackUsage(callCluster) { - return callCluster('transport.request', { - method: 'GET', - path: '/_xpack/usage', - query: { - master_timeout: TIMEOUT, - }, - }); -} - -/** - * Combine the X-Pack responses into a single response as Monitoring does already. - * - * @param {Object} license The license returned from /_license - * @param {Object} usage The usage details returned from /_xpack/usage - * @return {Object} An object containing both the license and usage. - */ -export function handleXPack(license, usage) { - return { - license, - stack_stats: { - xpack: usage, - }, - }; -} - -/** - * Combine all X-Pack requests as a singular request that is ignored upon failure. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} - */ -export function getXPack(callCluster) { - return Promise.all([getXPackLicense(callCluster), getXPackUsage(callCluster)]) - .then(([license, xpack]) => { - return { - license, - xpack, - }; - }) - .catch(() => { - return {}; - }); -} diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.ts new file mode 100644 index 0000000000000..9b69540007e5f --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.ts @@ -0,0 +1,25 @@ +/* + * 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 { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { TIMEOUT } from './constants'; + +/** + * Get the cluster stats from the connected cluster. + * + * This is the equivalent of GET /_xpack/usage?master_timeout=${TIMEOUT} + * + * Like any X-Pack related API, X-Pack must installed for this to work. + */ +export function getXPackUsage(callCluster: CallCluster) { + return callCluster('transport.request', { + method: 'GET', + path: '/_xpack/usage', + query: { + master_timeout: TIMEOUT, + }, + }); +} diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts index 57faf2da90d09..04445d7bde7d7 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/register_xpack_collection.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getLocalLicense } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_license'; import { telemetryCollectionManager } from '../../../../../../src/legacy/core_plugins/telemetry/server'; import { getClusterUuids } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection'; import { getStatsWithXpack } from './get_stats_with_xpack'; @@ -15,5 +16,6 @@ export function registerMonitoringCollection() { priority: 1, statsGetter: getStatsWithXpack, clusterDetailsGetter: getClusterUuids, + licenseGetter: getLocalLicense, }); } diff --git a/x-pack/package.json b/x-pack/package.json index f76b0182ea228..b8fe0326903b6 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -106,6 +106,7 @@ "@types/uuid": "^3.4.4", "@types/xml-crypto": "^1.4.0", "@types/xml2js": "^0.4.5", + "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "axios": "^0.19.0", "babel-jest": "^24.9.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index 62406cfaf66e1..01355f2a34f92 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -12,8 +12,6 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { withoutControlCharacters } from './lib/string_utils'; -const ACTION_NAME = 'Server log'; - // params definition export type ActionParamsType = TypeOf; @@ -37,7 +35,9 @@ const ParamsSchema = schema.object({ export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.server-log', - name: ACTION_NAME, + name: i18n.translate('xpack.actions.builtin.serverLogTitle', { + defaultMessage: 'Server log', + }), validate: { params: ParamsSchema, }, @@ -56,7 +56,7 @@ async function executor( const sanitizedMessage = withoutControlCharacters(params.message); try { - logger[params.level](`${ACTION_NAME}: ${sanitizedMessage}`); + logger[params.level](`Server log: ${sanitizedMessage}`); } catch (err) { const message = i18n.translate('xpack.actions.builtin.serverLog.errorLoggingErrorMessage', { defaultMessage: 'error logging message', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts index a6c43f48fa803..9ae96cb23a5c3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts @@ -49,7 +49,7 @@ beforeAll(() => { describe('get()', () => { test('should return correct action type', () => { expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual('servicenow'); + expect(actionType.name).toEqual('ServiceNow'); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts index 2d5c18207def3..0ad435281eba4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts @@ -89,7 +89,9 @@ export function getActionType({ }): ActionType { return { id: '.servicenow', - name: 'servicenow', + name: i18n.translate('xpack.actions.builtin.servicenowTitle', { + defaultMessage: 'ServiceNow', + }), validate: { config: schema.object(ConfigSchemaProps, { validate: curry(validateConfig)(configurationUtilities), diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 528aec2f70ad9..548b29346e483 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; +import { ILicense } from '../../licensing/public'; + export interface ServiceConnectionNode { 'service.name': string; 'service.environment': string | null; @@ -30,3 +33,18 @@ export interface ServiceNodeMetrics { avgRequestsPerMinute: number | null; avgErrorsPerMinute: number | null; } + +export function isValidPlatinumLicense(license: ILicense) { + return ( + license.isActive && + (license.type === 'platinum' || license.type === 'trial') + ); +} + +export const invalidLicenseMessage = i18n.translate( + 'xpack.apm.serviceMap.invalidLicenseMessage', + { + defaultMessage: + "In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data." + } +); diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 42232a8b89605..96579377c95e8 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -5,6 +5,6 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "apm"], "ui": false, - "requiredPlugins": ["apm_oss", "data", "home"], + "requiredPlugins": ["apm_oss", "data", "home", "licensing"], "optionalPlugins": ["cloud", "usageCollection"] } diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index 86eb1dba507f0..c22084dbb7168 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -12,8 +12,9 @@ import { IndicesCreateParams, DeleteDocumentResponse } from 'elasticsearch'; -import { cloneDeep, isString, merge, uniqueId } from 'lodash'; +import { cloneDeep, isString, merge } from 'lodash'; import { KibanaRequest } from 'src/core/server'; +import chalk from 'chalk'; import { ESSearchRequest, ESSearchResponse @@ -126,6 +127,10 @@ interface ClientCreateOptions { export type ESClient = ReturnType; +function formatObj(obj: Record) { + return JSON.stringify(obj, null, 2); +} + export function getESClient( context: APMRequestHandlerContext, request: KibanaRequest, @@ -136,25 +141,49 @@ export function getESClient( callAsInternalUser } = context.core.elasticsearch.dataClient; - const callMethod = clientAsInternalUser - ? callAsInternalUser - : callAsCurrentUser; + async function callEs(operationName: string, params: Record) { + const startTime = process.hrtime(); + + let res: any; + let esError = null; + try { + res = clientAsInternalUser + ? await callAsInternalUser(operationName, params) + : await callAsCurrentUser(operationName, params); + } catch (e) { + // catch error and throw after outputting debug info + esError = e; + } + + if (context.params.query._debug) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${ + request.route.path + }`; - const debug = context.params.query._debug; + console.log( + chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + ); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); - function withTime( - fn: (log: typeof console.log) => Promise - ): Promise { - const log = console.log.bind(console, uniqueId()); - if (!debug) { - return fn(log); + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); } - const time = process.hrtime(); - return fn(log).then(data => { - const now = process.hrtime(time); - log(`took: ${Math.round(now[0] * 1000 + now[1] / 1e6)}ms`); - return data; - }); + + if (esError) { + throw esError; + } + + return res; } return { @@ -171,40 +200,25 @@ export function getESClient( apmOptions ); - return withTime(log => { - if (context.params.query._debug) { - log(`--DEBUG ES QUERY--`); - log( - `${request.url.pathname} ${JSON.stringify(context.params.query)}` - ); - log(`GET ${nextParams.index}/_search`); - log(JSON.stringify(nextParams.body, null, 2)); - } - - return (callMethod('search', nextParams) as unknown) as Promise< - ESSearchResponse - >; - }); + return callEs('search', nextParams); }, index: (params: APMIndexDocumentParams) => { - return withTime(() => callMethod('index', params)); + return callEs('index', params); }, delete: (params: IndicesDeleteParams): Promise => { - return withTime(() => callMethod('delete', params)); + return callEs('delete', params); }, indicesCreate: (params: IndicesCreateParams) => { - return withTime(() => callMethod('indices.create', params)); + return callEs('indices.create', params); }, hasPrivileges: ( params: IndexPrivilegesParams ): Promise => { - return withTime(() => - callMethod('transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: params - }) - ); + return callEs('transport.request', { + method: 'POST', + path: '/_security/user/_has_privileges', + body: params + }); } }; } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index de23c4c5dd383..adc80cb43620b 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -19,6 +19,7 @@ import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { tutorialProvider } from './tutorial'; import { CloudSetup } from '../../cloud/server'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; +import { LicensingPluginSetup } from '../../licensing/public'; export interface LegacySetup { server: Server; @@ -44,6 +45,7 @@ export class APMPlugin implements Plugin { plugins: { apm_oss: APMOSSPlugin extends Plugin ? TSetup : never; home: HomeServerPluginSetup; + licensing: LicensingPluginSetup; cloud?: CloudSetup; usageCollection?: UsageCollectionSetup; } diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 584598805f8b3..bead0445d6ccc 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; import Boom from 'boom'; +import * as t from 'io-ts'; +import { + invalidLicenseMessage, + isValidPlatinumLicense +} from '../../common/service_map'; import { setupRequest } from '../lib/helpers/setup_request'; -import { createRoute } from './create_route'; -import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; +import { createRoute } from './create_route'; +import { rangeRt, uiFiltersRt } from './default_api_types'; export const serviceMapRoute = createRoute(() => ({ path: '/api/apm/service-map', @@ -26,6 +30,10 @@ export const serviceMapRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } + if (!isValidPlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(invalidLicenseMessage); + } + const setup = await setupRequest(context, request); const { query: { serviceName, environment, after } @@ -51,6 +59,9 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } + if (!isValidPlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(invalidLicenseMessage); + } const setup = await setupRequest(context, request); const { diff --git a/x-pack/plugins/data_enhanced/public/index.ts b/x-pack/plugins/data_enhanced/public/index.ts index 93b6b7a957182..927716aae9780 100644 --- a/x-pack/plugins/data_enhanced/public/index.ts +++ b/x-pack/plugins/data_enhanced/public/index.ts @@ -9,3 +9,5 @@ import { DataEnhancedPlugin, DataEnhancedSetup, DataEnhancedStart } from './plug export const plugin = () => new DataEnhancedPlugin(); export { DataEnhancedSetup, DataEnhancedStart }; + +export { ASYNC_SEARCH_STRATEGY, IAsyncSearchRequest, IAsyncSearchOptions } from './search'; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 14b5382bc85aa..4fe27d400f45f 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -8,6 +8,7 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; +import { ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider } from './search'; export interface DataEnhancedSetupDependencies { data: DataPublicPluginSetup; @@ -20,11 +21,14 @@ export type DataEnhancedSetup = ReturnType; export type DataEnhancedStart = ReturnType; export class DataEnhancedPlugin implements Plugin { - public setup(core: CoreSetup, plugins: DataEnhancedSetupDependencies) { - plugins.data.autocomplete.addQuerySuggestionProvider( + constructor() {} + + public setup(core: CoreSetup, { data }: DataEnhancedSetupDependencies) { + data.autocomplete.addQuerySuggestionProvider( KUERY_LANGUAGE_NAME, setupKqlQuerySuggestionProvider(core) ); + data.search.registerSearchStrategyProvider(ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider); } public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts new file mode 100644 index 0000000000000..95f2c9e477064 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { of } from 'rxjs'; +import { AbortController } from 'abort-controller'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { asyncSearchStrategyProvider } from './async_search_strategy'; +import { IAsyncSearchOptions } from './types'; +import { CoreStart } from 'kibana/public'; + +describe('Async search strategy', () => { + let mockCoreStart: MockedKeys; + const mockSearch = jest.fn(); + const mockRequest = { params: {}, serverStrategy: 'foo' }; + const mockOptions: IAsyncSearchOptions = { pollInterval: 0 }; + + beforeEach(() => { + mockCoreStart = coreMock.createStart(); + mockSearch.mockReset(); + }); + + it('only sends one request if the first response is complete', async () => { + mockSearch.mockReturnValueOnce(of({ id: 1, total: 1, loaded: 1 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + await asyncSearch.search(mockRequest, mockOptions).toPromise(); + + expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest); + expect(mockSearch.mock.calls[0][1]).toEqual({}); + expect(mockSearch).toBeCalledTimes(1); + }); + + it('stops polling when the response is complete', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch.search(mockRequest, mockOptions).toPromise(); + + expect(mockSearch).toBeCalledTimes(2); + }); + + it('only sends the ID and server strategy after the first request', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch.search(mockRequest, mockOptions).toPromise(); + + expect(mockSearch).toBeCalledTimes(2); + expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest); + expect(mockSearch.mock.calls[1][0]).toEqual({ id: 1, serverStrategy: 'foo' }); + }); + + it('sends a DELETE request and stops polling when the signal is aborted', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + const abortController = new AbortController(); + const options = { ...mockOptions, signal: abortController.signal }; + + const promise = asyncSearch.search(mockRequest, options).toPromise(); + abortController.abort(); + + try { + await promise; + } catch (e) { + expect(e.name).toBe('AbortError'); + expect(mockSearch).toBeCalledTimes(1); + expect(mockCoreStart.http.delete).toBeCalled(); + } + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts new file mode 100644 index 0000000000000..fa5d677a53b2a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts @@ -0,0 +1,75 @@ +/* + * 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 { EMPTY, fromEvent, NEVER, Observable, throwError, timer } from 'rxjs'; +import { mergeMap, expand, takeUntil } from 'rxjs/operators'; +import { + IKibanaSearchResponse, + ISearchContext, + ISearchStrategy, + SYNC_SEARCH_STRATEGY, + TSearchStrategyProvider, +} from '../../../../../src/plugins/data/public'; +import { IAsyncSearchRequest, IAsyncSearchOptions } from './types'; + +export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY'; + +declare module '../../../../../src/plugins/data/public' { + export interface IRequestTypesMap { + [ASYNC_SEARCH_STRATEGY]: IAsyncSearchRequest; + } +} + +export const asyncSearchStrategyProvider: TSearchStrategyProvider = ( + context: ISearchContext +): ISearchStrategy => { + const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY); + const { search } = syncStrategyProvider(context); + return { + search: ( + request: IAsyncSearchRequest, + { pollInterval = 1000, ...options }: IAsyncSearchOptions = {} + ): Observable => { + const { serverStrategy } = request; + let id: string | undefined = request.id; + + const aborted$ = options.signal + ? fromEvent(options.signal, 'abort').pipe( + mergeMap(() => { + // If we haven't received the response to the initial request, including the ID, then + // we don't need to send a follow-up request to delete this search. Otherwise, we + // send the follow-up request to delete this search, then throw an abort error. + if (id !== undefined) { + context.core.http.delete(`/internal/search/${request.serverStrategy}/${id}`); + } + + const error = new Error('Aborted'); + error.name = 'AbortError'; + return throwError(error); + }) + ) + : NEVER; + + return search(request, options).pipe( + expand(response => { + // If the response indicates it is complete, stop polling and complete the observable + if ((response.loaded ?? 0) >= (response.total ?? 0)) return EMPTY; + + id = response.id; + + // Delay by the given poll interval + return timer(pollInterval).pipe( + // Send future requests using just the ID from the response + mergeMap(() => { + return search({ id, serverStrategy }, options); + }) + ); + }), + takeUntil(aborted$) + ); + }, + }; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/index.ts b/x-pack/plugins/data_enhanced/public/search/index.ts new file mode 100644 index 0000000000000..a7729aeea5647 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider } from './async_search_strategy'; +export { IAsyncSearchRequest, IAsyncSearchOptions } from './types'; diff --git a/x-pack/plugins/data_enhanced/public/search/types.ts b/x-pack/plugins/data_enhanced/public/search/types.ts new file mode 100644 index 0000000000000..edaaf1b22654d --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ISearchOptions, ISyncSearchRequest } from '../../../../../src/plugins/data/public'; + +export interface IAsyncSearchRequest extends ISyncSearchRequest { + /** + * The ID received from the response from the initial request + */ + id?: string; +} + +export interface IAsyncSearchOptions extends ISearchOptions { + /** + * The number of milliseconds to wait between receiving a response and sending another request + */ + pollInterval?: number; +} diff --git a/x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx similarity index 61% rename from x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx rename to x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx index 06f134b10a4b7..0b9f54f51f61e 100644 --- a/x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx +++ b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx @@ -10,11 +10,11 @@ import { CoreStart } from 'src/core/public'; import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { FormCreateDrilldown } from '../../components/form_create_drilldown'; +import { FlyoutCreateDrilldown } from '../../components/flyout_create_drilldown'; export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN'; -interface ActionContext { +export interface FlyoutCreateDrilldownActionContext { embeddable: IEmbeddable; } @@ -22,29 +22,31 @@ export interface OpenFlyoutAddDrilldownParams { overlays: () => Promise; } -export class OpenFlyoutAddDrilldown implements Action { +export class FlyoutCreateDrilldownAction implements Action { public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; - public order = 100; + public order = 5; constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {} public getDisplayName() { - return i18n.translate('xpack.drilldowns.panel.openFlyoutAddDrilldown.displayName', { - defaultMessage: 'Add drilldown', + return i18n.translate('xpack.drilldowns.FlyoutCreateDrilldownAction.displayName', { + defaultMessage: 'Create Drilldown', }); } public getIconType() { - return 'empty'; + return 'plusInCircle'; } - public async isCompatible({ embeddable }: ActionContext) { + public async isCompatible({ embeddable }: FlyoutCreateDrilldownActionContext) { return true; } - public async execute({ embeddable }: ActionContext) { + public async execute(context: FlyoutCreateDrilldownActionContext) { const overlays = await this.params.overlays(); - overlays.openFlyout(toMountPoint()); + const handle = overlays.openFlyout( + toMountPoint( handle.close()} />) + ); } } diff --git a/x-pack/plugins/drilldowns/public/actions/index.ts b/x-pack/plugins/drilldowns/public/actions/index.ts index c0ca7fac22049..ce235043b4ef6 100644 --- a/x-pack/plugins/drilldowns/public/actions/index.ts +++ b/x-pack/plugins/drilldowns/public/actions/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './open_flyout_add_drilldown'; +export * from './flyout_create_drilldown'; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx similarity index 91% rename from x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx rename to x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx index afa82f5e74c16..7a9e19342f27c 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; -import { DrilldownHelloBar } from '..'; +import { DrilldownHelloBar } from '.'; storiesOf('components/DrilldownHelloBar', module).add('default', () => { return ; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx new file mode 100644 index 0000000000000..1ef714f7b86e2 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx @@ -0,0 +1,26 @@ +/* + * 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'; + +export interface DrilldownHelloBarProps { + docsLink?: string; +} + +/** + * @todo https://github.com/elastic/kibana/issues/55311 + */ +export const DrilldownHelloBar: React.FC = ({ docsLink }) => { + return ( +
+

+ Drilldowns provide the ability to define a new behavior when interacting with a panel. You + can add multiple options or simply override the default filtering behavior. +

+ View docs +
+ ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx index 895a100df3ac5..f28c8cfa3a059 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx @@ -4,24 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -export interface DrilldownHelloBarProps { - docsLink?: string; -} - -export const DrilldownHelloBar: React.FC = ({ docsLink }) => { - return ( -
-

- Drilldowns provide the ability to define a new behavior when interacting with a panel. You - can add multiple options or simply override the default filtering behavior. -

- View docs - -
- ); -}; +export * from './drilldown_hello_bar'; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx similarity index 91% rename from x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx rename to x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx index dfdd9627ab5cd..5627a5d6f4522 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; -import { DrilldownPicker } from '..'; +import { DrilldownPicker } from '.'; storiesOf('components/DrilldownPicker', module).add('default', () => { return ; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx new file mode 100644 index 0000000000000..3748fc666c81c --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +// eslint-disable-next-line +export interface DrilldownPickerProps {} + +export const DrilldownPicker: React.FC = () => { + return ( + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx index 3748fc666c81c..3be289fe6d46e 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx @@ -4,18 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - -// eslint-disable-next-line -export interface DrilldownPickerProps {} - -export const DrilldownPicker: React.FC = () => { - return ( - - ); -}; +export * from './drilldown_picker'; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx new file mode 100644 index 0000000000000..4f024b7d9cd6a --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-console */ + +import * as React from 'react'; +import { EuiFlyout } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { FlyoutCreateDrilldown } from '.'; + +storiesOf('components/FlyoutCreateDrilldown', module) + .add('default', () => { + return ; + }) + .add('open in flyout', () => { + return ( + + + + ); + }); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx new file mode 100644 index 0000000000000..b45ac9197c7e0 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import { FormCreateDrilldown } from '../form_create_drilldown'; +import { FlyoutFrame } from '../flyout_frame'; +import { txtCreateDrilldown } from './i18n'; +import { FlyoutCreateDrilldownActionContext } from '../../actions'; + +export interface FlyoutCreateDrilldownProps { + context: FlyoutCreateDrilldownActionContext; + onClose?: () => void; +} + +export const FlyoutCreateDrilldown: React.FC = ({ + context, + onClose, +}) => { + const footer = ( + {}} fill> + {txtCreateDrilldown} + + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts new file mode 100644 index 0000000000000..ceabc6d3a9aa5 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtCreateDrilldown = i18n.translate( + 'xpack.drilldowns.components.FlyoutCreateDrilldown.CreateDrilldown', + { + defaultMessage: 'Create drilldown', + } +); diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts similarity index 84% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts rename to x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts index 41bc2aa258807..ce235043b4ef6 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts +++ b/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts @@ -3,3 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +export * from './flyout_create_drilldown'; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.story.tsx new file mode 100644 index 0000000000000..2715637f6392f --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.story.tsx @@ -0,0 +1,39 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +import * as React from 'react'; +import { EuiFlyout, EuiButton } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { FlyoutFrame } from '.'; + +storiesOf('components/FlyoutFrame', module) + .add('default', () => { + return test; + }) + .add('with title', () => { + return test; + }) + .add('with onClose', () => { + return console.log('onClose')}>test; + }) + .add('custom footer', () => { + return click me!}>test; + }) + .add('open in flyout', () => { + return ( + + Save} + onClose={() => console.log('onClose')} + > + test + + + ); + }); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.test.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.test.tsx new file mode 100644 index 0000000000000..b5fb52fcf5c18 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { render } from 'react-dom'; +import { render as renderTestingLibrary, fireEvent } from '@testing-library/react'; +import { FlyoutFrame } from '.'; + +describe('', () => { + test('renders without crashing', () => { + const div = document.createElement('div'); + render(, div); + }); + + describe('[title=]', () => { + test('renders title in

tag', () => { + const div = document.createElement('div'); + render(, div); + + const title = div.querySelector('h1'); + expect(title?.textContent).toBe('foobar'); + }); + + test('title can be any react node', () => { + const div = document.createElement('div'); + render( + + foo bar + + } + />, + div + ); + + const title = div.querySelector('h1'); + expect(title?.innerHTML).toBe('foo bar'); + }); + }); + + describe('[footer=]', () => { + test('if [footer=] prop not provided, does not render footer', () => { + const div = document.createElement('div'); + render(, div); + + const footer = div.querySelector('[data-test-subj="flyoutFooter"]'); + expect(footer).toBe(null); + }); + + test('can render anything in footer', () => { + const div = document.createElement('div'); + render( + + a b + + } + />, + div + ); + + const footer = div.querySelector('[data-test-subj="flyoutFooter"]'); + expect(footer?.innerHTML).toBe('a b'); + }); + }); + + describe('[onClose=]', () => { + test('does not render close button if "onClose" prop is missing', () => { + const div = document.createElement('div'); + render(, div); + + const closeButton = div.querySelector('[data-test-subj="flyoutCloseButton"]'); + expect(closeButton).toBe(null); + }); + + test('renders close button if "onClose" prop is provided', () => { + const div = document.createElement('div'); + render( {}} />, div); + + const closeButton = div.querySelector('[data-test-subj="flyoutCloseButton"]'); + expect(closeButton).not.toBe(null); + }); + + test('calls onClose prop when close button clicked', async () => { + const onClose = jest.fn(); + const el = renderTestingLibrary(); + + const closeButton = el.queryByText('Close'); + + expect(onClose).toHaveBeenCalledTimes(0); + + fireEvent.click(closeButton!); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx new file mode 100644 index 0000000000000..2945cfd739482 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; +import { txtClose } from './i18n'; + +export interface FlyoutFrameProps { + title?: React.ReactNode; + footer?: React.ReactNode; + onClose?: () => void; +} + +/** + * @todo This component can be moved to `kibana_react`. + */ +export const FlyoutFrame: React.FC = ({ + title = '', + footer, + onClose, + children, +}) => { + const headerFragment = title && ( + + +

{title}

+
+
+ ); + + const footerFragment = (onClose || footer) && ( + + + + {onClose && ( + + {txtClose} + + )} + + + {footer} + + + + ); + + return ( + <> + {headerFragment} + {children} + {footerFragment} + + ); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts b/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts similarity index 61% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts rename to x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts index 020d6972f8280..257d7d36dbee1 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/index.ts +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UpgradeAssistantUIPlugin } from './plugin'; +import { i18n } from '@kbn/i18n'; -export const plugin = () => { - return new UpgradeAssistantUIPlugin(); -}; +export const txtClose = i18n.translate('xpack.drilldowns.components.FlyoutFrame.Close', { + defaultMessage: 'Close', +}); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/index.ts b/x-pack/plugins/drilldowns/public/components/flyout_frame/index.tsx similarity index 88% rename from x-pack/legacy/plugins/upgrade_assistant/public/index.ts rename to x-pack/plugins/drilldowns/public/components/flyout_frame/index.tsx index d22b5d64b6b46..040b4b6b5e243 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/index.ts +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './legacy'; +export * from './flyout_frame'; diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx deleted file mode 100644 index 34f6932b41dac..0000000000000 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx +++ /dev/null @@ -1,13 +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 * as React from 'react'; -import { storiesOf } from '@storybook/react'; -import { FormCreateDrilldown } from '..'; - -storiesOf('components/FormCreateDrilldown', module).add('default', () => { - return ; -}); diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx new file mode 100644 index 0000000000000..e7e1d67473e8c --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx @@ -0,0 +1,34 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +import * as React from 'react'; +import { EuiFlyout } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { FormCreateDrilldown } from '.'; + +const DemoEditName: React.FC = () => { + const [name, setName] = React.useState(''); + + return ; +}; + +storiesOf('components/FormCreateDrilldown', module) + .add('default', () => { + return ; + }) + .add('[name=foobar]', () => { + return ; + }) + .add('can edit name', () => ) + .add('open in flyout', () => { + return ( + + + + ); + }); diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx new file mode 100644 index 0000000000000..6691966e47e64 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render } from 'react-dom'; +import { FormCreateDrilldown } from '.'; +import { render as renderTestingLibrary, fireEvent } from '@testing-library/react'; +import { txtNameOfDrilldown } from './i18n'; + +describe('', () => { + test('renders without crashing', () => { + const div = document.createElement('div'); + render( {}} />, div); + }); + + describe('[name=]', () => { + test('if name not provided, uses to empty string', () => { + const div = document.createElement('div'); + + render(, div); + + const input = div.querySelector( + '[data-test-subj="dynamicActionNameInput"]' + ) as HTMLInputElement; + + expect(input?.value).toBe(''); + }); + + test('can set name input field value', () => { + const div = document.createElement('div'); + + render(, div); + + const input = div.querySelector( + '[data-test-subj="dynamicActionNameInput"]' + ) as HTMLInputElement; + + expect(input?.value).toBe('foo'); + + render(, div); + + expect(input?.value).toBe('bar'); + }); + + test('fires onNameChange callback on name change', () => { + const onNameChange = jest.fn(); + const utils = renderTestingLibrary( + + ); + const input = utils.getByLabelText(txtNameOfDrilldown); + + expect(onNameChange).toHaveBeenCalledTimes(0); + + fireEvent.change(input, { target: { value: 'qux' } }); + + expect(onNameChange).toHaveBeenCalledTimes(1); + expect(onNameChange).toHaveBeenCalledWith('qux'); + + fireEvent.change(input, { target: { value: 'quxx' } }); + + expect(onNameChange).toHaveBeenCalledTimes(2); + expect(onNameChange).toHaveBeenCalledWith('quxx'); + }); + }); +}); diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx new file mode 100644 index 0000000000000..4422de604092b --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { DrilldownHelloBar } from '../drilldown_hello_bar'; +import { txtNameOfDrilldown, txtUntitledDrilldown, txtDrilldownAction } from './i18n'; +import { DrilldownPicker } from '../drilldown_picker'; + +const noop = () => {}; + +export interface FormCreateDrilldownProps { + name?: string; + onNameChange?: (name: string) => void; +} + +export const FormCreateDrilldown: React.FC = ({ + name = '', + onNameChange = noop, +}) => { + const nameFragment = ( + + onNameChange(event.target.value)} + data-test-subj="dynamicActionNameInput" + /> + + ); + + const triggerPicker =
Trigger Picker will be here
; + const actionPicker = ( + + + + ); + + return ( + <> + + {nameFragment} + {triggerPicker} + {actionPicker} + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts index 922131ba4b901..4c0e287935edd 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts @@ -7,21 +7,21 @@ import { i18n } from '@kbn/i18n'; export const txtNameOfDrilldown = i18n.translate( - 'xpack.drilldowns.components.form_create_drilldown.nameOfDrilldown', + 'xpack.drilldowns.components.FormCreateDrilldown.nameOfDrilldown', { defaultMessage: 'Name of drilldown', } ); export const txtUntitledDrilldown = i18n.translate( - 'xpack.drilldowns.components.form_create_drilldown.untitledDrilldown', + 'xpack.drilldowns.components.FormCreateDrilldown.untitledDrilldown', { defaultMessage: 'Untitled drilldown', } ); export const txtDrilldownAction = i18n.translate( - 'xpack.drilldowns.components.form_create_drilldown.drilldownAction', + 'xpack.drilldowns.components.FormCreateDrilldown.drilldownAction', { defaultMessage: 'Drilldown action', } diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx index 40cd4cf2b210b..c2c5a7e435b39 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx @@ -4,27 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { DrilldownHelloBar } from '../drilldown_hello_bar'; -import { txtNameOfDrilldown, txtUntitledDrilldown, txtDrilldownAction } from './i18n'; -import { DrilldownPicker } from '../drilldown_picker'; - -// eslint-disable-next-line -export interface FormCreateDrilldownProps {} - -export const FormCreateDrilldown: React.FC = () => { - return ( -
- - - - - - - - - -
- ); -}; +export * from './form_create_drilldown'; diff --git a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts index f22f452181648..b1310ba003ff7 100644 --- a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts +++ b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts @@ -5,17 +5,17 @@ */ import { CoreSetup } from 'src/core/public'; -import { OpenFlyoutAddDrilldown } from '../actions/open_flyout_add_drilldown'; +import { FlyoutCreateDrilldownAction } from '../actions'; import { DrilldownsSetupDependencies } from '../plugin'; export class DrilldownService { bootstrap(core: CoreSetup, { uiActions }: DrilldownsSetupDependencies) { - const actionOpenFlyoutAddDrilldown = new OpenFlyoutAddDrilldown({ + const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays: async () => (await core.getStartServices())[0].overlays, }); - uiActions.registerAction(actionOpenFlyoutAddDrilldown); - uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionOpenFlyoutAddDrilldown.id); + uiActions.registerAction(actionFlyoutCreateDrilldown); + uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionFlyoutCreateDrilldown.id); } /** diff --git a/x-pack/plugins/drilldowns/scripts/storybook.js b/x-pack/plugins/drilldowns/scripts/storybook.js index 9b0f57746e584..2bfd0eb1a8f19 100644 --- a/x-pack/plugins/drilldowns/scripts/storybook.js +++ b/x-pack/plugins/drilldowns/scripts/storybook.js @@ -9,5 +9,5 @@ import { join } from 'path'; // eslint-disable-next-line require('@kbn/storybook').runStorybookCli({ name: 'drilldowns', - storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.examples.tsx')], + storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')], }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index ecefd4bfa271e..b61196439ee4f 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -173,6 +173,7 @@ describe('createIndex', () => { await clusterClientAdapter.createIndex('foo'); expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.create', { index: 'foo', + body: {}, }); }); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index c74eeacc9bb19..d585fd4f539b5 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -94,9 +94,12 @@ export class ClusterClientAdapter { return result as boolean; } - public async createIndex(name: string): Promise { + public async createIndex(name: string, body: any = {}): Promise { try { - await this.callEs('indices.create', { index: name }); + await this.callEs('indices.create', { + index: name, + body, + }); } catch (err) { if (err.body?.error?.type !== 'resource_already_exists_exception') { throw new Error(`error creating initial index: ${err.message}`); diff --git a/x-pack/plugins/event_log/server/es/documents.test.ts b/x-pack/plugins/event_log/server/es/documents.test.ts index 7edca4b3943a6..c08d0ac978bc9 100644 --- a/x-pack/plugins/event_log/server/es/documents.test.ts +++ b/x-pack/plugins/event_log/server/es/documents.test.ts @@ -22,8 +22,7 @@ describe('getIndexTemplate()', () => { test('returns the correct details of the index template', () => { const indexTemplate = getIndexTemplate(esNames); - expect(indexTemplate.index_patterns).toEqual([esNames.indexPattern]); - expect(indexTemplate.aliases[esNames.alias]).toEqual({}); + expect(indexTemplate.index_patterns).toEqual([esNames.indexPatternWithVersion]); expect(indexTemplate.settings.number_of_shards).toBeGreaterThanOrEqual(0); expect(indexTemplate.settings.number_of_replicas).toBeGreaterThanOrEqual(0); expect(indexTemplate.settings['index.lifecycle.name']).toBe(esNames.ilmPolicy); diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index 09dd7383c4c5e..982454e671008 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -10,10 +10,7 @@ import mappings from '../../generated/mappings.json'; // returns the body of an index template used in an ES indices.putTemplate call export function getIndexTemplate(esNames: EsNames) { const indexTemplateBody: any = { - index_patterns: [esNames.indexPattern], - aliases: { - [esNames.alias]: {}, - }, + index_patterns: [esNames.indexPatternWithVersion], settings: { number_of_shards: 1, number_of_replicas: 1, diff --git a/x-pack/plugins/event_log/server/es/init.ts b/x-pack/plugins/event_log/server/es/init.ts index 7094277f7aa9f..c67d6541ce002 100644 --- a/x-pack/plugins/event_log/server/es/init.ts +++ b/x-pack/plugins/event_log/server/es/init.ts @@ -62,7 +62,13 @@ class EsInitializationSteps { async createInitialIndexIfNotExists(): Promise { const exists = await this.esContext.esAdapter.doesAliasExist(this.esContext.esNames.alias); if (!exists) { - await this.esContext.esAdapter.createIndex(this.esContext.esNames.initialIndex); + await this.esContext.esAdapter.createIndex(this.esContext.esNames.initialIndex, { + aliases: { + [this.esContext.esNames.alias]: { + is_write_index: true, + }, + }, + }); } } } diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts index 7b013a0d263da..268421235b4b2 100644 --- a/x-pack/plugins/event_log/server/es/names.mock.ts +++ b/x-pack/plugins/event_log/server/es/names.mock.ts @@ -9,11 +9,12 @@ import { EsNames } from './names'; const createNamesMock = () => { const mock: jest.Mocked = { base: '.kibana', - alias: '.kibana-event-log', + alias: '.kibana-event-log-8.0.0', ilmPolicy: '.kibana-event-log-policy', indexPattern: '.kibana-event-log-*', - initialIndex: '.kibana-event-log-000001', - indexTemplate: '.kibana-event-log-template', + indexPatternWithVersion: '.kibana-event-log-8.0.0-*', + initialIndex: '.kibana-event-log-8.0.0-000001', + indexTemplate: '.kibana-event-log-8.0.0-template', }; return mock; }; diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts index d88c4212df91c..baefd756bb1ed 100644 --- a/x-pack/plugins/event_log/server/es/names.test.ts +++ b/x-pack/plugins/event_log/server/es/names.test.ts @@ -6,15 +6,21 @@ import { getEsNames } from './names'; +jest.mock('../lib/../../../../package.json', () => ({ + version: '1.2.3', +})); + describe('getEsNames()', () => { test('works as expected', () => { const base = 'XYZ'; + const version = '1.2.3'; const esNames = getEsNames(base); expect(esNames.base).toEqual(base); - expect(esNames.alias).toEqual(`${base}-event-log`); + expect(esNames.alias).toEqual(`${base}-event-log-${version}`); expect(esNames.ilmPolicy).toEqual(`${base}-event-log-policy`); expect(esNames.indexPattern).toEqual(`${base}-event-log-*`); - expect(esNames.initialIndex).toEqual(`${base}-event-log-000001`); - expect(esNames.indexTemplate).toEqual(`${base}-event-log-template`); + expect(esNames.indexPatternWithVersion).toEqual(`${base}-event-log-${version}-*`); + expect(esNames.initialIndex).toEqual(`${base}-event-log-${version}-000001`); + expect(esNames.indexTemplate).toEqual(`${base}-event-log-${version}-template`); }); }); diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts index be737d23625f1..d55d02a16fc9a 100644 --- a/x-pack/plugins/event_log/server/es/names.ts +++ b/x-pack/plugins/event_log/server/es/names.ts @@ -4,25 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -const EVENT_LOG_NAME_SUFFIX = '-event-log'; +import xPackage from '../../../../package.json'; + +const EVENT_LOG_NAME_SUFFIX = `-event-log`; +const EVENT_LOG_VERSION_SUFFIX = `-${xPackage.version}`; export interface EsNames { base: string; alias: string; ilmPolicy: string; indexPattern: string; + indexPatternWithVersion: string; initialIndex: string; indexTemplate: string; } export function getEsNames(baseName: string): EsNames { const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`; + const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`; return { base: baseName, - alias: eventLogName, + alias: eventLogNameWithVersion, ilmPolicy: `${eventLogName}-policy`, indexPattern: `${eventLogName}-*`, - initialIndex: `${eventLogName}-000001`, - indexTemplate: `${eventLogName}-template`, + indexPatternWithVersion: `${eventLogNameWithVersion}-*`, + initialIndex: `${eventLogNameWithVersion}-000001`, + indexTemplate: `${eventLogNameWithVersion}-template`, }; } diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index 66e699abd22b4..a797e4c9d4ba7 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -26,6 +26,7 @@ import { KibanaContextProvider, } from '../../../../../src/plugins/kibana_react/public'; import { AppRouter } from '../routers'; +import '../index.scss'; export const CONTAINER_CLASSNAME = 'infra-container-element'; @@ -74,12 +75,8 @@ export async function startApp( ); - // Ensure the element we're handed from application mounting takes up - // the full size it can, so that our inner application styles work as - // expected. - element.style.height = '100%'; - element.style.display = 'flex'; - element.style.overflowY = 'hidden'; // Prevent having scroll within a container having scroll. It messes up with drag-n-drop elements + // Ensure the element we're handed from application mounting is assigned a class + // for our index.scss styles to apply to. element.className += ` ${CONTAINER_CLASSNAME}`; ReactDOM.render(, element); diff --git a/x-pack/plugins/infra/public/components/source_configuration/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts index 4879a53ca329d..98825567cc204 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/index.ts +++ b/x-pack/plugins/infra/public/components/source_configuration/index.ts @@ -5,7 +5,4 @@ */ export { SourceConfigurationSettings } from './source_configuration_settings'; -export { - ViewSourceConfigurationButton, - ViewSourceConfigurationButtonHrefBase, -} from './view_source_configuration_button'; +export { ViewSourceConfigurationButton } from './view_source_configuration_button'; diff --git a/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx b/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx index 9b584b2ef3bd0..9c3a40fb7ecf0 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx @@ -8,23 +8,16 @@ import { EuiButton } from '@elastic/eui'; import React from 'react'; import { Route } from 'react-router-dom'; -export enum ViewSourceConfigurationButtonHrefBase { - infrastructure = 'infrastructure', - logs = 'logs', -} - interface ViewSourceConfigurationButtonProps { 'data-test-subj'?: string; - hrefBase: ViewSourceConfigurationButtonHrefBase; children: React.ReactNode; } export const ViewSourceConfigurationButton = ({ 'data-test-subj': dataTestSubj, - hrefBase, children, }: ViewSourceConfigurationButtonProps) => { - const href = `/${hrefBase}/settings`; + const href = '/settings'; return ( { {uiCapabilities?.infrastructure?.configureSource ? ( - + {i18n.translate('xpack.infra.configureSourceActionLabel', { defaultMessage: 'Change source configuration', })} diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index 1294007240027..739bad5689a96 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -10,10 +10,7 @@ import { identity } from 'fp-ts/lib/function'; import React from 'react'; import { NoIndices } from '../../../components/empty_states/no_indices'; -import { - ViewSourceConfigurationButton, - ViewSourceConfigurationButtonHrefBase, -} from '../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const LogsPageNoIndicesContent = () => { @@ -49,10 +46,7 @@ export const LogsPageNoIndicesContent = () => { {canConfigureSource ? ( - + {i18n.translate('xpack.infra.configureSourceActionLabel', { defaultMessage: 'Change source configuration', })} diff --git a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx index fde3b61de50b5..43f684cd5a585 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx @@ -10,10 +10,7 @@ import { identity } from 'fp-ts/lib/function'; import React from 'react'; import { euiStyled } from '../../../../../observability/public'; -import { - ViewSourceConfigurationButton, - ViewSourceConfigurationButtonHrefBase, -} from '../../../components/source_configuration'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface InvalidNodeErrorProps { @@ -59,10 +56,7 @@ export const InvalidNodeError: React.FunctionComponent = - + void; + serverBasePath: string; onManageSpacesClick: () => void; intl: InjectedIntl; capabilities: Capabilities; @@ -184,7 +184,7 @@ class SpacesMenuUI extends Component { diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control.tsx index 53d7038cec4d5..4d7af256822c8 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control.tsx @@ -23,6 +23,7 @@ export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreSta { const wrapper = shallow( { const wrapper = mountWithIntl( { id={popoutContentId} spaces={this.state.spaces} isLoading={this.state.loading} - onSelectSpace={this.onSelectSpace} + serverBasePath={this.props.serverBasePath} onManageSpacesClick={this.toggleSpaceSelector} capabilities={this.props.capabilities} navigateToApp={this.props.navigateToApp} @@ -175,8 +176,4 @@ export class NavControlPopover extends Component { showSpaceSelector: false, }); }; - - private onSelectSpace = (space: Space) => { - this.props.spacesManager.changeSelectedSpace(space); - }; } diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 73dae84c1873b..44215ec538002 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -43,8 +43,7 @@ export class SpacesPlugin implements Plugin { const space = { @@ -16,21 +17,33 @@ test('it renders without crashing', () => { disabledFeatures: [], }; - shallow(); + shallow(); }); -test('it is clickable', () => { +test('links to the indicated space', () => { const space = { - id: '', + id: 'some-space', name: 'space name', description: 'space description', disabledFeatures: [], }; - const clickHandler = jest.fn(); + const wrapper = mount(); + expect(wrapper.find(EuiCard).props()).toMatchObject({ + href: '/server-base-path/s/some-space/spaces/enter', + }); +}); - const wrapper = mount(); - wrapper.find('button').simulate('click'); +test('links to the default space too', () => { + const space = { + id: 'default', + name: 'default space', + description: 'space description', + disabledFeatures: [], + }; - expect(clickHandler).toHaveBeenCalledTimes(1); + const wrapper = mount(); + expect(wrapper.find(EuiCard).props()).toMatchObject({ + href: '/server-base-path/spaces/enter', + }); }); diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx index f898ba87c60bd..7a056427fb166 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx @@ -6,15 +6,16 @@ import { EuiCard } from '@elastic/eui'; import React from 'react'; -import { Space } from '../../../common/model/space'; +import { addSpaceIdToPath, ENTER_SPACE_PATH } from '../../../common'; import { SpaceAvatar } from '../../space_avatar'; +import { Space } from '../..'; interface Props { space: Space; - onClick: () => void; + serverBasePath: string; } export const SpaceCard = (props: Props) => { - const { space, onClick } = props; + const { serverBasePath, space } = props; return ( { icon={renderSpaceAvatar(space)} title={space.name} description={renderSpaceDescription(space)} - onClick={onClick} + href={addSpaceIdToPath(serverBasePath, space.id, ENTER_SPACE_PATH)} /> ); }; diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx index 07b85114e3c8f..8de22bcbe235f 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_cards.test.tsx @@ -16,5 +16,5 @@ test('it renders without crashing', () => { disabledFeatures: [], }; - shallow(); + shallow(); }); diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx index 7c4084d36b21d..b480cf524304f 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx @@ -11,7 +11,7 @@ import { SpaceCard } from './space_card'; interface Props { spaces: Space[]; - onSpaceSelect: (space: Space) => void; + serverBasePath: string; } export class SpaceCards extends Component { @@ -25,15 +25,9 @@ export class SpaceCards extends Component { ); } - public renderSpace = (space: Space) => ( + private renderSpace = (space: Space) => ( - + ); - - public createSpaceClickHandler = (space: Space) => { - return () => { - this.props.onSpaceSelect(space); - }; - }; } diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx index c8173de1661be..112a8a9a0a685 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx @@ -18,7 +18,9 @@ function getSpacesManager(spaces: Space[] = []) { test('it renders without crashing', () => { const spacesManager = getSpacesManager(); - const component = shallowWithIntl(); + const component = shallowWithIntl( + + ); expect(component).toMatchSnapshot(); }); @@ -34,7 +36,9 @@ test('it queries for spaces when loaded', () => { const spacesManager = getSpacesManager(spaces); - shallowWithIntl(); + shallowWithIntl( + + ); return Promise.resolve().then(() => { expect(spacesManager.getSpaces).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx index b63de399b95c9..9289784b9f95f 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx @@ -30,6 +30,7 @@ import { SpacesManager } from '../spaces_manager'; interface Props { spacesManager: SpacesManager; + serverBasePath: string; } interface State { @@ -129,7 +130,7 @@ export class SpaceSelector extends Component { {this.state.loading && } {!this.state.loading && ( - + )} {!this.state.loading && filteredSpaces.length === 0 && ( @@ -179,10 +180,6 @@ export class SpaceSelector extends Component { searchTerm: searchTerm.trim().toLowerCase(), }); }; - - public onSelectSpace = (space: Space) => { - this.props.spacesManager.changeSelectedSpace(space); - }; } export const renderSpaceSelectorApp = (i18nStart: CoreStart['i18n'], el: Element, props: Props) => { diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx index 29ead8d34994d..6fab1767e4b6d 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector_app.tsx @@ -29,7 +29,10 @@ export const spaceSelectorApp = Object.freeze({ getStartServices(), import('./space_selector'), ]); - return renderSpaceSelectorApp(coreStart.i18n, params.element, { spacesManager }); + return renderSpaceSelectorApp(coreStart.i18n, params.element, { + spacesManager, + serverBasePath: coreStart.http.basePath.serverBasePath, + }); }, }); }, diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index 56879af33916f..6186ac7fd93be 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -20,7 +20,6 @@ function createSpacesManagerMock() { copySavedObjects: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), - changeSelectedSpace: jest.fn(), } as unknown) as jest.Mocked; } diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts index f89bfc32a69e6..508669361c23f 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.test.ts @@ -12,14 +12,14 @@ describe('SpacesManager', () => { describe('#constructor', () => { it('attempts to retrieve the active space', () => { const coreStart = coreMock.createStart(); - new SpacesManager('/server-base-path', coreStart.http); + new SpacesManager(coreStart.http); expect(coreStart.http.get).toHaveBeenCalledWith('/internal/spaces/_active_space'); }); it('does not retrieve the active space if on an anonymous path', () => { const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); - new SpacesManager('/server-base-path', coreStart.http); + new SpacesManager(coreStart.http); expect(coreStart.http.get).not.toHaveBeenCalled(); }); }); @@ -31,7 +31,7 @@ describe('SpacesManager', () => { id: 'my-space', name: 'my space', }); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(coreStart.http.get).toHaveBeenCalledWith('/internal/spaces/_active_space'); await nextTick(); @@ -47,7 +47,7 @@ describe('SpacesManager', () => { it('throws if on an anonymous path', () => { const coreStart = coreMock.createStart(); coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(coreStart.http.get).not.toHaveBeenCalled(); expect(() => spacesManager.getActiveSpace()).toThrowErrorMatchingInlineSnapshot( @@ -67,7 +67,7 @@ describe('SpacesManager', () => { name: 'my other space', }); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(coreStart.http.get).toHaveBeenCalledWith('/internal/spaces/_active_space'); await nextTick(); @@ -95,7 +95,7 @@ describe('SpacesManager', () => { name: 'my space', }); - const spacesManager = new SpacesManager('/server-base-path', coreStart.http); + const spacesManager = new SpacesManager(coreStart.http); expect(() => spacesManager.getActiveSpace({ forceRefresh: true }) diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index e151dcd4f9368..cc3f51b1850de 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -9,16 +9,18 @@ import { HttpSetup } from 'src/core/public'; import { SavedObjectsManagementRecord } from 'src/legacy/core_plugins/management/public'; import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; -import { ENTER_SPACE_PATH } from '../../common/constants'; import { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types'; -import { addSpaceIdToPath } from '../../common'; export class SpacesManager { private activeSpace$: BehaviorSubject = new BehaviorSubject(null); + private readonly serverBasePath: string; + public readonly onActiveSpaceChange$: Observable; - constructor(private readonly serverBasePath: string, private readonly http: HttpSetup) { + constructor(private readonly http: HttpSetup) { + this.serverBasePath = http.basePath.serverBasePath; + this.onActiveSpaceChange$ = this.activeSpace$ .asObservable() .pipe(skipWhile((v: Space | null) => v == null)) as Observable; @@ -99,10 +101,6 @@ export class SpacesManager { }); } - public async changeSelectedSpace(space: Space) { - window.location.href = addSpaceIdToPath(this.serverBasePath, space.id, ENTER_SPACE_PATH); - } - public redirectToSpaceSelector() { window.location.href = `${this.serverBasePath}/spaces/space_selector`; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dc97a96d4fcba..4a627d48c3cf0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2872,7 +2872,6 @@ "visualizations.newVisWizard.title": "新規ビジュアライゼーション", "visualizations.newVisWizard.visTypeAliasDescription": "Visualize外でKibanaアプリケーションを開きます。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibanaアプリケーション", - "visualizations.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした", "visDefaultEditor.aggSelect.aggregationLabel": "集約", "visDefaultEditor.aggSelect.helpLinkLabel": "{aggTitle} のヘルプ", "visDefaultEditor.aggSelect.noCompatibleAggsDescription": "インデックスパターン{indexPatternTitle}には集約可能なフィールドが含まれていません。", @@ -12782,12 +12781,9 @@ "xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "ダウン", "xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "アップ", "xpack.uptime.pluginDescription": "アップタイム監視", - "xpack.uptime.snapshot.downCountsMessage": "{down}/{total} 個のモニターがダウンしています", "xpack.uptime.snapshot.noDataDescription": "申し訳ございませんが、ヒストグラムに利用可能なデータがありません", "xpack.uptime.snapshot.noDataTitle": "ヒストグラムデータがありません", - "xpack.uptime.snapshot.noMonitorMessage": "モニターが見つかりません", "xpack.uptime.snapshot.pingsOverTimeTitle": "一定時間のピング", - "xpack.uptime.snapshot.zeroDownMessage": "すべてのモニターが起動しています", "xpack.uptime.snapshotHistogram.description": "{startTime} から {endTime} までの期間のアップタイムステータスを表示する棒グラフです。", "xpack.uptime.snapshotHistogram.downMonitorsId": "ダウンモニター", "xpack.uptime.snapshotHistogram.series.downLabel": "ダウン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2532cdb0c4d07..e0ef4a7a1ebdb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2873,7 +2873,6 @@ "visualizations.newVisWizard.title": "新建可视化", "visualizations.newVisWizard.visTypeAliasDescription": "打开 Visualize 外部的 Kibana 应用程序。", "visualizations.newVisWizard.visTypeAliasTitle": "Kibana 应用程序", - "visualizations.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界", "visDefaultEditor.aggSelect.aggregationLabel": "聚合", "visDefaultEditor.aggSelect.helpLinkLabel": "{aggTitle} 帮助", "visDefaultEditor.aggSelect.noCompatibleAggsDescription": "索引模式“{indexPatternTitle}”不包含任何聚合。", @@ -12782,12 +12781,9 @@ "xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "关闭", "xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "运行", "xpack.uptime.pluginDescription": "运行时间监测", - "xpack.uptime.snapshot.downCountsMessage": "{down}/{total} 个监测已关闭", "xpack.uptime.snapshot.noDataDescription": "抱歉,没有可用于该直方图的数据", "xpack.uptime.snapshot.noDataTitle": "没有可用的直方图数据", - "xpack.uptime.snapshot.noMonitorMessage": "未找到任何监测", "xpack.uptime.snapshot.pingsOverTimeTitle": "时移 Ping 数", - "xpack.uptime.snapshot.zeroDownMessage": "所有监测已启动", "xpack.uptime.snapshotHistogram.description": "显示从 {startTime} 到 {endTime} 的运行时间时移状态的条形图。", "xpack.uptime.snapshotHistogram.downMonitorsId": "已关闭监测", "xpack.uptime.snapshotHistogram.series.downLabel": "关闭", diff --git a/x-pack/plugins/upgrade_assistant/common/config.ts b/x-pack/plugins/upgrade_assistant/common/config.ts new file mode 100644 index 0000000000000..8a13aedd5fdd8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/common/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type Config = TypeOf; diff --git a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts similarity index 88% rename from x-pack/legacy/plugins/upgrade_assistant/common/types.ts rename to x-pack/plugins/upgrade_assistant/common/types.ts index 0e65506bb584d..a0c12154988a1 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; export enum ReindexStep { @@ -114,3 +115,15 @@ export interface UpgradeAssistantTelemetry { export interface UpgradeAssistantTelemetrySavedObjectAttributes { [key: string]: any; } + +export interface EnrichedDeprecationInfo extends DeprecationInfo { + index?: string; + node?: string; + reindex?: boolean; +} + +export interface UpgradeAssistantStatus { + readyForUpgrade: boolean; + cluster: EnrichedDeprecationInfo[]; + indices: EnrichedDeprecationInfo[]; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/common/version.ts b/x-pack/plugins/upgrade_assistant/common/version.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/common/version.ts rename to x-pack/plugins/upgrade_assistant/common/version.ts diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json new file mode 100644 index 0000000000000..273036a653aeb --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "upgradeAssistant", + "version": "kibana", + "server": true, + "ui": true, + "configPath": ["xpack", "upgrade_assistant"], + "requiredPlugins": ["management", "licensing"], + "optionalPlugins": ["cloud", "usageCollection"] +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss b/x-pack/plugins/upgrade_assistant/public/application/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/index.scss rename to x-pack/plugins/upgrade_assistant/public/application/_index.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx new file mode 100644 index 0000000000000..17eff71f1039b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx @@ -0,0 +1,44 @@ +/* + * 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 { I18nStart } from 'src/core/public'; +import { EuiPageHeader, EuiPageHeaderSection, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NEXT_MAJOR_VERSION } from '../../common/version'; +import { UpgradeAssistantTabs } from './components/tabs'; +import { AppContextProvider, ContextValue, AppContext } from './app_context'; + +export interface AppDependencies extends ContextValue { + i18n: I18nStart; +} + +export const RootComponent = ({ i18n, ...contexValue }: AppDependencies) => { + return ( + + +
+ + + +

+ +

+
+
+
+ + {({ http }) => } + +
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx rename to x-pack/plugins/upgrade_assistant/public/application/app_context.tsx index a48a4efa3bbdf..1ae9dabd69481 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/app_context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx @@ -9,7 +9,6 @@ import React, { createContext, useContext } from 'react'; export interface ContextValue { http: HttpSetup; isCloudEnabled: boolean; - XSRF: string; } export const AppContext = createContext({} as any); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/error_banner.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/error_banner.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/error_banner.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx index 864df292fbffe..43d0364425cbb 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/latest_minor_banner.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../common/version'; +import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../common/version'; export const LatestMinorBanner: React.FunctionComponent = () => ( ({ - get: jest.fn(), - create: jest.fn(), -})); - +import { httpServiceMock } from 'src/core/public/mocks'; import { UpgradeAssistantTabs } from './tabs'; import { LoadingState } from './types'; -import axios from 'axios'; import { OverviewTab } from './tabs/overview'; // Used to wait for promises to resolve and renders to finish before reading updates const promisesToResolve = () => new Promise(resolve => setTimeout(resolve, 0)); -const mockHttp = { - basePath: { - prepend: () => 'test', - }, - fetch() {}, -}; +const mockHttp = httpServiceMock.createSetupContract(); describe('UpgradeAssistantTabs', () => { test('renders loading state', async () => { - // @ts-ignore - axios.get.mockReturnValue( + mockHttp.get.mockReturnValue( new Promise(resolve => { /* never resolve */ }) ); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); // Should pass down loading status to child component expect(wrapper.find(OverviewTab).prop('loadingState')).toEqual(LoadingState.Loading); }); test('successful data fetch', async () => { // @ts-ignore - axios.get.mockResolvedValue({ + mockHttp.get.mockResolvedValue({ data: { cluster: [], indices: [], }, }); const wrapper = mountWithIntl(); - expect(axios.get).toHaveBeenCalled(); + expect(mockHttp.get).toHaveBeenCalled(); await promisesToResolve(); wrapper.update(); // Should pass down success status to child component @@ -59,7 +47,7 @@ describe('UpgradeAssistantTabs', () => { test('network failure', async () => { // @ts-ignore - axios.get.mockRejectedValue(new Error(`oh no!`)); + mockHttp.get.mockRejectedValue(new Error(`oh no!`)); const wrapper = mountWithIntl(); await promisesToResolve(); wrapper.update(); @@ -69,7 +57,7 @@ describe('UpgradeAssistantTabs', () => { it('upgrade error', async () => { // @ts-ignore - axios.get.mockRejectedValue({ response: { status: 426 } }); + mockHttp.get.mockRejectedValue({ response: { status: 426 } }); const wrapper = mountWithIntl(); await promisesToResolve(); wrapper.update(); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 0b154fb20404d..43ec5554aaaee 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; import { findIndex, get, set } from 'lodash'; import React from 'react'; @@ -18,7 +17,7 @@ import { import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { HttpSetup } from 'src/core/public'; -import { UpgradeAssistantStatus } from '../../../../server/np_ready/lib/es_migration_apis'; +import { UpgradeAssistantStatus } from '../../../common/types'; import { LatestMinorBanner } from './latest_minor_banner'; import { CheckupTab } from './tabs/checkup'; import { OverviewTab } from './tabs/overview'; @@ -153,12 +152,10 @@ export class UpgradeAssistantTabsUI extends React.Component { private loadData = async () => { try { this.setState({ loadingState: LoadingState.Loading }); - const resp = await axios.get( - this.props.http.basePath.prepend('/api/upgrade_assistant/status') - ); + const resp = await this.props.http.get('/api/upgrade_assistant/status'); this.setState({ loadingState: LoadingState.Success, - checkupData: resp.data, + checkupData: resp, }); } catch (e) { if (get(e, 'response.status') === 426) { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/__fixtures__/checkup_api_response.json b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/__fixtures__/checkup_api_response.json similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/__fixtures__/checkup_api_response.json rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/__fixtures__/checkup_api_response.json diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx index 7e862a846290b..b047427174e08 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/checkup_tab.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NEXT_MAJOR_VERSION } from '../../../../../../common/version'; +import { NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { LoadingErrorBanner } from '../../error_banner'; import { GroupByOption, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/constants.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/controls.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/controls.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_cell.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_cell.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_cell.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_cell.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_deprecations.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_deprecations.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_deprecations.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_deprecations.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx similarity index 94% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx index 4bd2f7c4bf62c..879bb695ca60a 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/cell.tsx @@ -79,9 +79,7 @@ export const DeprecationCell: FunctionComponent = ({ {reindexIndexName && ( - {({ http, XSRF }) => ( - - )} + {({ http }) => } )} diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx similarity index 93% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx index a0e55dc55c865..7e5172a361a56 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/count_summary.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/count_summary.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FunctionComponent } from 'react'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; export const DeprecationCountSummary: FunctionComponent<{ deprecations: EnrichedDeprecationInfo[]; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx index 28f5f6894b78f..c6309fb57d786 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx @@ -11,7 +11,7 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { EuiBadge, EuiPagination } from '@elastic/eui'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx index 74f66b6c4fb35..8fa78639c39d3 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/grouped.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx @@ -19,7 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationCountSummary } from './count_summary'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/health.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx index 835affce59070..5506528a3ded0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/index_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx @@ -148,9 +148,7 @@ export class IndexDeprecationTableUI extends React.Component< render(indexDep: IndexDeprecationDetails) { return ( - {({ XSRF, http }) => ( - - )} + {({ http }) => } ); }, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx index 78ded73593464..a1e173737bab0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption } from '../../../types'; import { DeprecationList } from './list'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx index 15a3d94974dcd..a46bc0d12fad4 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx @@ -7,7 +7,7 @@ import React, { FunctionComponent } from 'react'; import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; -import { EnrichedDeprecationInfo } from '../../../../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption } from '../../../types'; import { COLOR_MAP, LEVEL_MAP } from '../constants'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_button.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_button.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_button.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_button.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx index 2a28018a3ae81..30b46e0c15213 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx @@ -11,14 +11,13 @@ import { Subscription } from 'rxjs'; import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { HttpSetup } from 'src/core/public'; -import { ReindexStatus, UIReindexOption } from '../../../../../../../../common/types'; +import { ReindexStatus, UIReindexOption } from '../../../../../../../common/types'; import { LoadingState } from '../../../../types'; import { ReindexFlyout } from './flyout'; import { ReindexPollingService, ReindexState } from './polling_service'; interface ReindexButtonProps { indexName: string; - xsrf: string; http: HttpSetup; } @@ -154,8 +153,8 @@ export class ReindexButton extends React.Component { diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx index 91e35c0bd7dc0..643dd2e9b6efc 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx @@ -21,7 +21,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ReindexWarning } from '../../../../../../../../../common/types'; +import { ReindexWarning } from '../../../../../../../../common/types'; interface CheckedIds { [id: string]: boolean; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts similarity index 63% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts index cb2a0856f0f2e..4228426d62159 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockClient } from './polling_service.test.mocks'; - -import { ReindexStatus, ReindexStep } from '../../../../../../../../common/types'; +import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; import { httpServiceMock } from 'src/core/public/http/http_service.mock'; +const mockClient = httpServiceMock.createSetupContract(); + describe('ReindexPollingService', () => { beforeEach(() => { mockClient.post.mockReset(); @@ -18,18 +18,11 @@ describe('ReindexPollingService', () => { it('does not poll when reindexOp is null', async () => { mockClient.get.mockResolvedValueOnce({ - status: 200, - data: { - warnings: [], - reindexOp: null, - }, + warnings: [], + reindexOp: null, }); - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -39,22 +32,15 @@ describe('ReindexPollingService', () => { it('does not poll when first check is a 200 and status is failed', async () => { mockClient.get.mockResolvedValue({ - status: 200, - data: { - warnings: [], - reindexOp: { - lastCompletedStep: ReindexStep.created, - status: ReindexStatus.failed, - errorMessage: `Oh no!`, - }, + warnings: [], + reindexOp: { + lastCompletedStep: ReindexStep.created, + status: ReindexStatus.failed, + errorMessage: `Oh no!`, }, }); - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -65,21 +51,14 @@ describe('ReindexPollingService', () => { it('begins to poll when first check is a 200 and status is inProgress', async () => { mockClient.get.mockResolvedValue({ - status: 200, - data: { - warnings: [], - reindexOp: { - lastCompletedStep: ReindexStep.created, - status: ReindexStatus.inProgress, - }, + warnings: [], + reindexOp: { + lastCompletedStep: ReindexStep.created, + status: ReindexStatus.inProgress, }, }); - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); service.updateStatus(); await new Promise(resolve => setTimeout(resolve, 1200)); // wait for poll interval @@ -89,11 +68,7 @@ describe('ReindexPollingService', () => { describe('startReindex', () => { it('posts to endpoint', async () => { - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); await service.startReindex(); expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex'); @@ -102,11 +77,7 @@ describe('ReindexPollingService', () => { describe('cancelReindex', () => { it('posts to cancel endpoint', async () => { - const service = new ReindexPollingService( - 'myIndex', - 'myXsrf', - httpServiceMock.createSetupContract() - ); + const service = new ReindexPollingService('myIndex', mockClient); await service.cancelReindex(); expect(mockClient.post).toHaveBeenCalledWith('/api/upgrade_assistant/reindex/myIndex/cancel'); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts similarity index 82% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts index 879fafe610982..6fe6a85905706 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/deprecations/reindex/polling_service.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import axios, { AxiosInstance } from 'axios'; - import { BehaviorSubject } from 'rxjs'; import { HttpSetup } from 'src/core/public'; @@ -14,7 +12,7 @@ import { ReindexStatus, ReindexStep, ReindexWarning, -} from '../../../../../../../../common/types'; +} from '../../../../../../../common/types'; import { LoadingState } from '../../../../types'; const POLL_INTERVAL = 1000; @@ -45,24 +43,13 @@ interface StatusResponse { export class ReindexPollingService { public status$: BehaviorSubject; private pollTimeout?: NodeJS.Timeout; - private APIClient: AxiosInstance; - constructor(private indexName: string, private xsrf: string, private http: HttpSetup) { + constructor(private indexName: string, private http: HttpSetup) { this.status$ = new BehaviorSubject({ loadingState: LoadingState.Loading, errorMessage: null, reindexTaskPercComplete: null, }); - - this.APIClient = axios.create({ - headers: { - Accept: 'application/json', - credentials: 'same-origin', - 'Content-Type': 'application/json', - 'kbn-version': this.xsrf, - 'kbn-xsrf': this.xsrf, - }, - }); } public updateStatus = async () => { @@ -70,8 +57,8 @@ export class ReindexPollingService { this.stopPolling(); try { - const { data } = await this.APIClient.get( - this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}`) + const data = await this.http.get( + `/api/upgrade_assistant/reindex/${this.indexName}` ); this.updateWithResponse(data); @@ -107,8 +94,8 @@ export class ReindexPollingService { errorMessage: null, cancelLoadingState: undefined, }); - const { data } = await this.APIClient.post( - this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}`) + const data = await this.http.post( + `/api/upgrade_assistant/reindex/${this.indexName}` ); this.updateWithResponse({ reindexOp: data }); @@ -125,9 +112,7 @@ export class ReindexPollingService { cancelLoadingState: LoadingState.Loading, }); - await this.APIClient.post( - this.http.basePath.prepend(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`) - ); + await this.http.post(`/api/upgrade_assistant/reindex/${this.indexName}/cancel`); } catch (e) { this.status$.next({ ...this.status$.value, diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/filter_bar.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.test.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.test.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/group_by_bar.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/group_by_bar.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/index.tsx similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/index.tsx diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_index.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_index.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_index.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_steps.scss b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_steps.scss similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/_steps.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/_steps.scss diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx similarity index 86% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx index db37bc58904ec..0e6c79dc47b53 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/deprecation_logging_toggle.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; import React from 'react'; import { EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; @@ -15,7 +14,6 @@ import { HttpSetup } from 'src/core/public'; import { LoadingState } from '../../types'; interface DeprecationLoggingTabProps extends ReactIntl.InjectedIntlProps { - xsrf: string; http: HttpSetup; } @@ -88,12 +86,10 @@ export class DeprecationLoggingToggleUI extends React.Component< private loadData = async () => { try { this.setState({ loadingState: LoadingState.Loading }); - const resp = await axios.get( - this.props.http.basePath.prepend('/api/upgrade_assistant/deprecation_logging') - ); + const resp = await this.props.http.get('/api/upgrade_assistant/deprecation_logging'); this.setState({ loadingState: LoadingState.Success, - loggingEnabled: resp.data.isEnabled, + loggingEnabled: resp.isEnabled, }); } catch (e) { this.setState({ loadingState: LoadingState.Error }); @@ -102,26 +98,19 @@ export class DeprecationLoggingToggleUI extends React.Component< private toggleLogging = async () => { try { - const { http, xsrf } = this.props; // Optimistically toggle the UI const newEnabled = !this.state.loggingEnabled; this.setState({ loadingState: LoadingState.Loading, loggingEnabled: newEnabled }); - const resp = await axios.put( - http.basePath.prepend('/api/upgrade_assistant/deprecation_logging'), - { + const resp = await this.props.http.put('/api/upgrade_assistant/deprecation_logging', { + body: JSON.stringify({ isEnabled: newEnabled, - }, - { - headers: { - 'kbn-xsrf': xsrf, - }, - } - ); + }), + }); this.setState({ loadingState: LoadingState.Success, - loggingEnabled: resp.data.isEnabled, + loggingEnabled: resp.isEnabled, }); } catch (e) { this.setState({ loadingState: LoadingState.Error }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx similarity index 96% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx index 284265bb31f14..aede377fa8d45 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/index.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/index.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NEXT_MAJOR_VERSION } from '../../../../../../common/version'; +import { NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { LoadingErrorBanner } from '../../error_banner'; import { LoadingState, UpgradeAssistantTabProps } from '../../types'; import { Steps } from './steps'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx index ccba51c73c136..85d275b080e13 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/steps.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../../../common/version'; +import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../../../common/version'; import { UpgradeAssistantTabProps } from '../../types'; import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; import { useAppContext } from '../../../app_context'; @@ -104,7 +104,7 @@ export const StepsUI: FunctionComponent - + ), diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts similarity index 89% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/types.ts index 2d9a373f20b7e..86d1486543596 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts @@ -6,10 +6,7 @@ import React from 'react'; -import { - EnrichedDeprecationInfo, - UpgradeAssistantStatus, -} from '../../../../server/np_ready/lib/es_migration_apis'; +import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../../common/types'; export interface UpgradeAssistantTabProps { alertBanner?: React.ReactNode; diff --git a/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx b/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx new file mode 100644 index 0000000000000..97120cfc3333a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx @@ -0,0 +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. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppDependencies, RootComponent } from './app'; + +interface BootDependencies extends AppDependencies { + element: HTMLElement; +} + +export const renderApp = (deps: BootDependencies) => { + const { element, ...appDependencies } = deps; + render(, element); + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.test.ts b/x-pack/plugins/upgrade_assistant/public/application/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.test.ts rename to x-pack/plugins/upgrade_assistant/public/application/utils.test.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.ts b/x-pack/plugins/upgrade_assistant/public/application/utils.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/utils.ts rename to x-pack/plugins/upgrade_assistant/public/application/utils.ts diff --git a/x-pack/plugins/upgrade_assistant/public/index.scss b/x-pack/plugins/upgrade_assistant/public/index.scss new file mode 100644 index 0000000000000..9bd47b6473372 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/index.scss @@ -0,0 +1 @@ +@import './application/index'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts b/x-pack/plugins/upgrade_assistant/public/index.ts similarity index 62% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts rename to x-pack/plugins/upgrade_assistant/public/index.ts index cf1b78e1e3920..2b1860167ef5d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/index.ts @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; -import { UpgradeAssistantServerPlugin } from './plugin'; +import './index.scss'; +import { PluginInitializerContext } from 'src/core/public'; +import { UpgradeAssistantUIPlugin } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => { - return new UpgradeAssistantServerPlugin(); + return new UpgradeAssistantUIPlugin(ctx); }; diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts new file mode 100644 index 0000000000000..614221272dd5c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -0,0 +1,48 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; + +import { CloudSetup } from '../../cloud/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; + +import { NEXT_MAJOR_VERSION } from '../common/version'; +import { Config } from '../common/config'; + +import { renderApp } from './application/render_app'; + +interface Dependencies { + cloud: CloudSetup; + management: ManagementSetup; +} + +export class UpgradeAssistantUIPlugin implements Plugin { + constructor(private ctx: PluginInitializerContext) {} + setup({ http, getStartServices }: CoreSetup, { cloud, management }: Dependencies) { + const { enabled } = this.ctx.config.get(); + if (!enabled) { + return; + } + const appRegistrar = management.sections.getSection('elasticsearch')!; + const isCloudEnabled = Boolean(cloud?.isCloudEnabled); + + appRegistrar.registerApp({ + id: 'upgrade_assistant', + title: i18n.translate('xpack.upgradeAssistant.appTitle', { + defaultMessage: '{version} Upgrade Assistant', + values: { version: `${NEXT_MAJOR_VERSION}.0` }, + }), + order: 1000, + async mount({ element }) { + const [{ i18n: i18nDep }] = await getStartServices(); + return renderApp({ element, isCloudEnabled, http, i18n: i18nDep }); + }, + }); + } + + start() {} + stop() {} +} diff --git a/x-pack/plugins/upgrade_assistant/server/index.ts b/x-pack/plugins/upgrade_assistant/server/index.ts new file mode 100644 index 0000000000000..cab7eb613f74c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { UpgradeAssistantServerPlugin } from './plugin'; +import { configSchema } from '../common/config'; + +export const plugin = (ctx: PluginInitializerContext) => { + return new UpgradeAssistantServerPlugin(ctx); +}; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + enabled: true, + }, +}; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__fixtures__/fake_deprecations.json b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__fixtures__/fake_deprecations.json rename to x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__mocks__/es_version_precheck.ts b/x-pack/plugins/upgrade_assistant/server/lib/__mocks__/es_version_precheck.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__mocks__/es_version_precheck.ts rename to x-pack/plugins/upgrade_assistant/server/lib/__mocks__/es_version_precheck.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__snapshots__/es_migration_apis.test.ts.snap b/x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/__snapshots__/es_migration_apis.test.ts.snap rename to x-pack/plugins/upgrade_assistant/server/lib/__snapshots__/es_migration_apis.test.ts.snap diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts index 317e2a7554e03..862f64e232370 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { getDeprecationLoggingStatus, isDeprecationLoggingEnabled, @@ -12,9 +12,9 @@ import { describe('getDeprecationLoggingStatus', () => { it('calls cluster.getSettings', async () => { - const callWithRequest = jest.fn(); - await getDeprecationLoggingStatus(callWithRequest, {} as any); - expect(callWithRequest).toHaveBeenCalledWith({}, 'cluster.getSettings', { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + await getDeprecationLoggingStatus(dataClient); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.getSettings', { includeDefaults: true, }); }); @@ -23,9 +23,9 @@ describe('getDeprecationLoggingStatus', () => { describe('setDeprecationLogging', () => { describe('isEnabled = true', () => { it('calls cluster.putSettings with logger.deprecation = WARN', async () => { - const callWithRequest = jest.fn(); - await setDeprecationLogging(callWithRequest, {} as any, true); - expect(callWithRequest).toHaveBeenCalledWith({}, 'cluster.putSettings', { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + await setDeprecationLogging(dataClient, true); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { body: { transient: { 'logger.deprecation': 'WARN' } }, }); }); @@ -33,9 +33,9 @@ describe('setDeprecationLogging', () => { describe('isEnabled = false', () => { it('calls cluster.putSettings with logger.deprecation = ERROR', async () => { - const callWithRequest = jest.fn(); - await setDeprecationLogging(callWithRequest, {} as any, false); - expect(callWithRequest).toHaveBeenCalledWith({}, 'cluster.putSettings', { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + await setDeprecationLogging(dataClient, false); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { body: { transient: { 'logger.deprecation': 'ERROR' } }, }); }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts index 199d389408442..8f25533c0afb1 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_deprecation_logging_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts @@ -4,19 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; - -import { CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch'; -import { RequestShim } from '../types'; +import { IScopedClusterClient } from 'src/core/server'; interface DeprecationLoggingStatus { isEnabled: boolean; } export async function getDeprecationLoggingStatus( - callWithRequest: CallClusterWithRequest, - req: RequestShim + dataClient: IScopedClusterClient ): Promise { - const response = await callWithRequest(req, 'cluster.getSettings', { + const response = await dataClient.callAsCurrentUser('cluster.getSettings', { includeDefaults: true, }); @@ -26,11 +23,10 @@ export async function getDeprecationLoggingStatus( } export async function setDeprecationLogging( - callWithRequest: CallClusterWithRequest, - req: RequestShim, + dataClient: IScopedClusterClient, isEnabled: boolean ): Promise { - const response = await callWithRequest(req, 'cluster.putSettings', { + const response = await dataClient.callAsCurrentUser('cluster.putSettings', { body: { transient: { 'logger.deprecation': isEnabled ? 'WARN' : 'ERROR', diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts similarity index 73% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index a1d7049e4171f..4ab4227ba3e91 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -5,6 +5,7 @@ */ import _ from 'lodash'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { getUpgradeAssistantStatus } from './es_migration_apis'; import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; @@ -13,7 +14,8 @@ import fakeDeprecations from './__fixtures__/fake_deprecations.json'; describe('getUpgradeAssistantStatus', () => { let deprecationsResponse: DeprecationAPIResponse; - const callWithRequest = jest.fn().mockImplementation(async (req, api, { path }) => { + const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + (dataClient.callAsCurrentUser as jest.Mock).mockImplementation(async (api, { path }) => { if (path === '/_migration/deprecations') { return deprecationsResponse; } else if (api === 'indices.getMapping') { @@ -28,15 +30,15 @@ describe('getUpgradeAssistantStatus', () => { }); it('calls /_migration/deprecations', async () => { - await getUpgradeAssistantStatus(callWithRequest, {} as any, false); - expect(callWithRequest).toHaveBeenCalledWith({}, 'transport.request', { + await getUpgradeAssistantStatus(dataClient, false); + expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('transport.request', { path: '/_migration/deprecations', method: 'GET', }); }); it('returns the correct shape of data', async () => { - const resp = await getUpgradeAssistantStatus(callWithRequest, {} as any, false); + const resp = await getUpgradeAssistantStatus(dataClient, false); expect(resp).toMatchSnapshot(); }); @@ -48,9 +50,10 @@ describe('getUpgradeAssistantStatus', () => { index_settings: {}, }; - await expect( - getUpgradeAssistantStatus(callWithRequest, {} as any, false) - ).resolves.toHaveProperty('readyForUpgrade', false); + await expect(getUpgradeAssistantStatus(dataClient, false)).resolves.toHaveProperty( + 'readyForUpgrade', + false + ); }); it('returns readyForUpgrade === true when no critical issues found', async () => { @@ -61,9 +64,10 @@ describe('getUpgradeAssistantStatus', () => { index_settings: {}, }; - await expect( - getUpgradeAssistantStatus(callWithRequest, {} as any, false) - ).resolves.toHaveProperty('readyForUpgrade', true); + await expect(getUpgradeAssistantStatus(dataClient, false)).resolves.toHaveProperty( + 'readyForUpgrade', + true + ); }); it('filters out security realm deprecation on Cloud', async () => { @@ -80,7 +84,7 @@ describe('getUpgradeAssistantStatus', () => { index_settings: {}, }; - const result = await getUpgradeAssistantStatus(callWithRequest, {} as any, true); + const result = await getUpgradeAssistantStatus(dataClient, true); expect(result).toHaveProperty('readyForUpgrade', true); expect(result).toHaveProperty('cluster', []); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts similarity index 75% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index b52c4c374266f..68f21c1fd93b5 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -4,32 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CallClusterWithRequest, - DeprecationAPIResponse, - DeprecationInfo, -} from 'src/legacy/core_plugins/elasticsearch'; - -import { RequestShim } from '../types'; - -export interface EnrichedDeprecationInfo extends DeprecationInfo { - index?: string; - node?: string; - reindex?: boolean; -} - -export interface UpgradeAssistantStatus { - readyForUpgrade: boolean; - cluster: EnrichedDeprecationInfo[]; - indices: EnrichedDeprecationInfo[]; -} +import { IScopedClusterClient } from 'src/core/server'; +import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; +import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../common/types'; export async function getUpgradeAssistantStatus( - callWithRequest: CallClusterWithRequest, - req: RequestShim, + dataClient: IScopedClusterClient, isCloudEnabled: boolean ): Promise { - const deprecations = await callWithRequest(req, 'transport.request', { + const deprecations = await dataClient.callAsCurrentUser('transport.request', { path: '/_migration/deprecations', method: 'GET', }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts index bbabe557df4d4..51cb776ef4e0b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts @@ -6,7 +6,7 @@ import { SemVer } from 'semver'; import { IScopedClusterClient, kibanaResponseFactory } from 'src/core/server'; -import { CURRENT_VERSION } from '../../../common/version'; +import { CURRENT_VERSION } from '../../common/version'; import { esVersionCheck, getAllNodeVersions, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts similarity index 98% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts rename to x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts index 2fb3effe43793..e7636eea66479 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/es_version_precheck.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.ts @@ -13,7 +13,7 @@ import { RequestHandler, RequestHandlerContext, } from 'src/core/server'; -import { CURRENT_VERSION } from '../../../common/version'; +import { CURRENT_VERSION } from '../../common/version'; /** * Returns an array of all the unique Elasticsearch Node Versions in the Elasticsearch cluster. diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts index 06fa755472238..ce892df0de946 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReindexSavedObject } from '../../../../common/types'; +import { ReindexSavedObject } from '../../../common/types'; import { Credential, credentialStoreFactory } from './credential_store'; describe('credentialStore', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts similarity index 91% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts index a051d16b5779f..0958559910be6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/credential_store.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts @@ -5,12 +5,11 @@ */ import { createHash } from 'crypto'; -import { Request } from 'hapi'; import stringify from 'json-stable-stringify'; -import { ReindexSavedObject } from '../../../../common/types'; +import { ReindexSavedObject } from '../../../common/types'; -export type Credential = Request['headers']; +export type Credential = Record; /** * An in-memory cache for user credentials to be used for reindexing operations. When looking up diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/index.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts index 7b346cc87edf6..9ec06b72f02e2 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; import { generateNewIndexName, getReindexWarnings, diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts index 0b95bc628fbb4..f6dc471d0945d 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/index_settings.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts @@ -5,8 +5,8 @@ */ import { flow, omit } from 'lodash'; -import { ReindexWarning } from '../../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +import { ReindexWarning } from '../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; import { FlatSettings } from './types'; export interface ParsedIndexName { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts similarity index 99% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 3fb855958a5d0..4569fdfa33a83 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -13,8 +13,8 @@ import { ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +} from '../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; import { LOCK_WINDOW, ReindexActions, reindexActionsFactory } from './reindex_actions'; describe('ReindexActions', () => { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts similarity index 97% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index 6683f80c8e779..2ae340f12d80c 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -6,8 +6,7 @@ import moment from 'moment'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsFindResponse, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsFindResponse, SavedObjectsClientContract, APICaller } from 'src/core/server'; import { IndexGroup, REINDEX_OP_TYPE, @@ -15,7 +14,7 @@ import { ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../../common/types'; +} from '../../../common/types'; import { generateNewIndexName } from './index_settings'; import { FlatSettings } from './types'; @@ -111,7 +110,7 @@ export interface ReindexActions { export const reindexActionsFactory = ( client: SavedObjectsClientContract, - callCluster: CallCluster + callAsUser: APICaller ): ReindexActions => { // ----- Internal functions const isLocked = (reindexOp: ReindexSavedObject) => { @@ -230,7 +229,7 @@ export const reindexActionsFactory = ( }, async getFlatSettings(indexName: string) { - const flatSettings = (await callCluster('transport.request', { + const flatSettings = (await callAsUser('transport.request', { path: `/${encodeURIComponent(indexName)}?flat_settings=true`, })) as { [indexName: string]: FlatSettings }; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts similarity index 95% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index 9cd41c8cbe826..6c3b2c869dc7f 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -4,14 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BehaviorSubject } from 'rxjs'; +import { Logger } from 'src/core/server'; +import { loggingServiceMock } from 'src/core/server/mocks'; + import { IndexGroup, ReindexOperation, ReindexSavedObject, ReindexStatus, ReindexStep, -} from '../../../../common/types'; -import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../../common/version'; +} from '../../../common/types'; +import { CURRENT_MAJOR_VERSION, PREV_MAJOR_VERSION } from '../../../common/version'; +import { licensingMock } from '../../../../licensing/server/mocks'; +import { LicensingPluginSetup } from '../../../../licensing/server'; + import { isMlIndex, isWatcherIndex, @@ -22,9 +29,9 @@ import { describe('reindexService', () => { let actions: jest.Mocked; let callCluster: jest.Mock; - let log: jest.Mock; - let xpackInfo: { feature: jest.Mocked }; + let log: Logger; let service: ReindexService; + let licensingPluginSetup: LicensingPluginSetup; const updateMockImpl = (reindexOp: ReindexSavedObject, attrs: Partial = {}) => Promise.resolve({ @@ -50,32 +57,24 @@ describe('reindexService', () => { runWhileIndexGroupLocked: jest.fn(async (group: string, f: any) => f({ attributes: {} })), }; callCluster = jest.fn(); - log = jest.fn(); - xpackInfo = { - feature: jest.fn(() => ({ - isAvailable() { - return true; - }, - isEnabled() { - return true; - }, - })), - }; - - service = reindexServiceFactory(callCluster as any, xpackInfo as any, actions, log); + log = loggingServiceMock.create().get(); + licensingPluginSetup = licensingMock.createSetup(); + licensingPluginSetup.license$ = new BehaviorSubject( + licensingMock.createLicense({ + features: { security: { isAvailable: true, isEnabled: true } }, + }) + ); + + service = reindexServiceFactory(callCluster as any, actions, log, licensingPluginSetup); }); describe('hasRequiredPrivileges', () => { it('returns true if security is disabled', async () => { - xpackInfo.feature.mockReturnValueOnce({ - isAvailable() { - return true; - }, - isEnabled() { - return false; - }, - }); - + licensingPluginSetup.license$ = new BehaviorSubject( + licensingMock.createLicense({ + features: { security: { isAvailable: true, isEnabled: false } }, + }) + ); const hasRequired = await service.hasRequiredPrivileges('anIndex'); expect(hasRequired).toBe(true); }); @@ -584,7 +583,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', @@ -599,7 +598,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', @@ -623,7 +622,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not stop ML jobs') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', @@ -645,7 +644,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Some nodes are not on minimum version') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); // Should not have called ML endpoint at all expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', @@ -698,7 +697,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_stop', method: 'POST', @@ -713,7 +712,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_stop', method: 'POST', @@ -735,7 +734,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not stop Watcher') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_watcher/_stop', method: 'POST', @@ -771,7 +770,7 @@ describe('reindexService', () => { ); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); it('fails if setting updates fail', async () => { @@ -782,7 +781,7 @@ describe('reindexService', () => { ); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -817,7 +816,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); it('fails if create index fails', async () => { @@ -829,7 +828,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); // Original index should have been set back to allow reads. expect(callCluster).toHaveBeenCalledWith('indices.putSettings', { @@ -874,7 +873,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.newIndexCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -931,7 +930,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexStarted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -1014,7 +1013,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexCompleted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); it('fails if switching aliases fails', async () => { @@ -1023,7 +1022,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.reindexCompleted); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); }); }); @@ -1092,7 +1091,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', @@ -1108,7 +1107,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', @@ -1129,7 +1128,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not resume ML jobs') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', @@ -1196,7 +1195,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_start', method: 'POST', @@ -1212,7 +1211,7 @@ describe('reindexService', () => { expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).not.toHaveBeenCalledWith('transport.request', { path: '/_watcher/_start', method: 'POST', @@ -1233,7 +1232,7 @@ describe('reindexService', () => { expect( updatedOp.attributes.errorMessage!.includes('Could not start Watcher') ).toBeTruthy(); - expect(log).toHaveBeenCalledWith(['upgrade_assistant', 'error'], expect.any(String)); + expect(log.error).toHaveBeenCalledWith(expect.any(String)); expect(callCluster).toHaveBeenCalledWith('transport.request', { path: '/_watcher/_start', method: 'POST', diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts similarity index 90% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 0e6095f98b6ff..8f1df5b34372b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -5,17 +5,16 @@ */ import Boom from 'boom'; +import { APICaller, Logger } from 'src/core/server'; +import { first } from 'rxjs/operators'; -import { Server } from 'hapi'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { XPackInfo } from '../../../../../xpack_main/server/lib/xpack_info'; import { IndexGroup, ReindexSavedObject, ReindexStatus, ReindexStep, ReindexWarning, -} from '../../../../common/types'; +} from '../../../common/types'; import { generateNewIndexName, getReindexWarnings, @@ -23,6 +22,7 @@ import { transformFlatSettings, } from './index_settings'; import { ReindexActions } from './reindex_actions'; +import { LicensingPluginSetup } from '../../../../licensing/server'; const VERSION_REGEX = new RegExp(/^([1-9]+)\.([0-9]+)\.([0-9]+)/); const ML_INDICES = ['.ml-state', '.ml-anomalies', '.ml-config']; @@ -97,10 +97,10 @@ export interface ReindexService { } export const reindexServiceFactory = ( - callCluster: CallCluster, - xpackInfo: XPackInfo, + callAsUser: APICaller, actions: ReindexActions, - log: Server['log'] + log: Logger, + licensing: LicensingPluginSetup ): ReindexService => { // ------ Utility functions @@ -114,7 +114,7 @@ export const reindexServiceFactory = ( await actions.runWhileIndexGroupLocked(IndexGroup.ml, async mlDoc => { await validateNodesMinimumVersion(6, 7); - const res = await callCluster('transport.request', { + const res = await callAsUser('transport.request', { path: '/_ml/set_upgrade_mode?enabled=true', method: 'POST', }); @@ -134,7 +134,7 @@ export const reindexServiceFactory = ( await actions.decrementIndexGroupReindexes(IndexGroup.ml); await actions.runWhileIndexGroupLocked(IndexGroup.ml, async mlDoc => { if (mlDoc.attributes.runningReindexCount === 0) { - const res = await callCluster('transport.request', { + const res = await callAsUser('transport.request', { path: '/_ml/set_upgrade_mode?enabled=false', method: 'POST', }); @@ -154,7 +154,7 @@ export const reindexServiceFactory = ( const stopWatcher = async () => { await actions.incrementIndexGroupReindexes(IndexGroup.watcher); await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async watcherDoc => { - const { acknowledged } = await callCluster('transport.request', { + const { acknowledged } = await callAsUser('transport.request', { path: '/_watcher/_stop', method: 'POST', }); @@ -174,7 +174,7 @@ export const reindexServiceFactory = ( await actions.decrementIndexGroupReindexes(IndexGroup.watcher); await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async watcherDoc => { if (watcherDoc.attributes.runningReindexCount === 0) { - const { acknowledged } = await callCluster('transport.request', { + const { acknowledged } = await callAsUser('transport.request', { path: '/_watcher/_start', method: 'POST', }); @@ -191,14 +191,14 @@ export const reindexServiceFactory = ( const cleanupChanges = async (reindexOp: ReindexSavedObject) => { // Cancel reindex task if it was started but not completed if (reindexOp.attributes.lastCompletedStep === ReindexStep.reindexStarted) { - await callCluster('tasks.cancel', { + await callAsUser('tasks.cancel', { taskId: reindexOp.attributes.reindexTaskId, }).catch(e => undefined); // Ignore any exceptions trying to cancel (it may have already completed). } // Set index back to writable if we ever got past this point. if (reindexOp.attributes.lastCompletedStep >= ReindexStep.readonly) { - await callCluster('indices.putSettings', { + await callAsUser('indices.putSettings', { index: reindexOp.attributes.indexName, body: { 'index.blocks.write': false }, }); @@ -208,7 +208,7 @@ export const reindexServiceFactory = ( reindexOp.attributes.lastCompletedStep >= ReindexStep.newIndexCreated && reindexOp.attributes.lastCompletedStep < ReindexStep.aliasCreated ) { - await callCluster('indices.delete', { index: reindexOp.attributes.newIndexName }); + await callAsUser('indices.delete', { index: reindexOp.attributes.newIndexName }); } // Resume consumers if we ever got past this point. @@ -222,7 +222,7 @@ export const reindexServiceFactory = ( // ------ Functions used to process the state machine const validateNodesMinimumVersion = async (minMajor: number, minMinor: number) => { - const nodesResponse = await callCluster('transport.request', { + const nodesResponse = await callAsUser('transport.request', { path: '/_nodes', method: 'GET', }); @@ -263,7 +263,7 @@ export const reindexServiceFactory = ( */ const setReadonly = async (reindexOp: ReindexSavedObject) => { const { indexName } = reindexOp.attributes; - const putReadonly = await callCluster('indices.putSettings', { + const putReadonly = await callAsUser('indices.putSettings', { index: indexName, body: { 'index.blocks.write': true }, }); @@ -289,7 +289,7 @@ export const reindexServiceFactory = ( const { settings, mappings } = transformFlatSettings(flatSettings); - const createIndex = await callCluster('indices.create', { + const createIndex = await callAsUser('indices.create', { index: newIndexName, body: { settings, @@ -313,7 +313,7 @@ export const reindexServiceFactory = ( const startReindexing = async (reindexOp: ReindexSavedObject) => { const { indexName } = reindexOp.attributes; - const startReindex = (await callCluster('reindex', { + const startReindex = (await callAsUser('reindex', { refresh: true, waitForCompletion: false, body: { @@ -337,7 +337,7 @@ export const reindexServiceFactory = ( const taskId = reindexOp.attributes.reindexTaskId; // Check reindexing task progress - const taskResponse = await callCluster('tasks.get', { + const taskResponse = await callAsUser('tasks.get', { taskId, waitForCompletion: false, }); @@ -358,7 +358,7 @@ export const reindexServiceFactory = ( reindexOp = await cleanupChanges(reindexOp); } else { // Check that it reindexed all documents - const { count } = await callCluster('count', { index: reindexOp.attributes.indexName }); + const { count } = await callAsUser('count', { index: reindexOp.attributes.indexName }); if (taskResponse.task.status.created < count) { // Include the entire task result in the error message. This should be guaranteed @@ -374,7 +374,7 @@ export const reindexServiceFactory = ( } // Delete the task from ES .tasks index - const deleteTaskResp = await callCluster('delete', { + const deleteTaskResp = await callAsUser('delete', { index: '.tasks', id: taskId, }); @@ -394,7 +394,7 @@ export const reindexServiceFactory = ( const { indexName, newIndexName } = reindexOp.attributes; const existingAliases = ( - await callCluster('indices.getAlias', { + await callAsUser('indices.getAlias', { index: indexName, }) )[indexName].aliases; @@ -403,7 +403,7 @@ export const reindexServiceFactory = ( add: { index: newIndexName, alias: aliasName, ...existingAliases[aliasName] }, })); - const aliasResponse = await callCluster('indices.updateAliases', { + const aliasResponse = await callAsUser('indices.updateAliases', { body: { actions: [ { add: { index: newIndexName, alias: indexName } }, @@ -443,9 +443,18 @@ export const reindexServiceFactory = ( return { async hasRequiredPrivileges(indexName: string) { + /** + * To avoid a circular dependency on Security we use a work around + * here to detect whether Security is available and enabled + * (i.e., via the licensing plugin). This enables Security to use + * functionality exposed through Upgrade Assistant. + */ + const license = await licensing.license$.pipe(first()).toPromise(); + + const securityFeature = license.getFeature('security'); + // If security is disabled or unavailable, return true. - const security = xpackInfo.feature('security'); - if (!security.isAvailable() || !security.isEnabled()) { + if (!securityFeature || !(securityFeature.isAvailable && securityFeature.isEnabled)) { return true; } @@ -482,7 +491,7 @@ export const reindexServiceFactory = ( body.cluster = [...body.cluster, 'manage_watcher']; } - const resp = await callCluster('transport.request', { + const resp = await callAsUser('transport.request', { path: '/_security/user/_has_privileges', method: 'POST', body, @@ -509,7 +518,7 @@ export const reindexServiceFactory = ( }, async createReindexOperation(indexName: string) { - const indexExists = await callCluster('indices.exists', { index: indexName }); + const indexExists = await callAsUser('indices.exists', { index: indexName }); if (!indexExists) { throw Boom.notFound(`Index ${indexName} does not exist in this cluster.`); } @@ -579,10 +588,7 @@ export const reindexServiceFactory = ( break; } } catch (e) { - log( - ['upgrade_assistant', 'error'], - `Reindexing step failed: ${e instanceof Error ? e.stack : e.toString()}` - ); + log.error(`Reindexing step failed: ${e instanceof Error ? e.stack : e.toString()}`); // Trap the exception and add the message to the object so the UI can display it. lockedReindexOp = await actions.updateReindexOp(lockedReindexOp, { @@ -647,7 +653,7 @@ export const reindexServiceFactory = ( throw new Error(`Reindex operation is not current waiting for reindex task to complete`); } - const resp = await callCluster('tasks.cancel', { + const resp = await callAsUser('tasks.cancel', { taskId: reindexOp.attributes.reindexTaskId, }); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/types.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/types.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts similarity index 76% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts rename to x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts index 628a47be9f5e7..bad6db62efe41 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/reindexing/worker.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts @@ -3,23 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster, CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch'; -import { Request, Server } from 'src/legacy/server/kbn_server'; -import { SavedObjectsClientContract } from 'kibana/server'; - +import { IClusterClient, Logger, SavedObjectsClientContract, FakeRequest } from 'src/core/server'; import moment from 'moment'; -import { XPackInfo } from '../../../../../xpack_main/server/lib/xpack_info'; -import { ReindexSavedObject, ReindexStatus } from '../../../../common/types'; + +import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { CredentialStore } from './credential_store'; import { reindexActionsFactory } from './reindex_actions'; import { ReindexService, reindexServiceFactory } from './reindex_service'; +import { LicensingPluginSetup } from '../../../../licensing/server'; const POLL_INTERVAL = 30000; // If no nodes have been able to update this index in 2 minutes (due to missing credentials), set to paused. const PAUSE_WINDOW = POLL_INTERVAL * 4; -const LOG_TAGS = ['upgrade_assistant', 'reindex_worker']; - /** * A singleton worker that will coordinate two polling loops: * (1) A longer loop that polls for reindex operations that are in progress. If any are found, loop (2) is started. @@ -41,24 +37,27 @@ export class ReindexWorker { private timeout?: NodeJS.Timeout; private inProgressOps: ReindexSavedObject[] = []; private readonly reindexService: ReindexService; + private readonly log: Logger; constructor( private client: SavedObjectsClientContract, private credentialStore: CredentialStore, - private callWithRequest: CallClusterWithRequest, - private callWithInternalUser: CallCluster, - private xpackInfo: XPackInfo, - private readonly log: Server['log'] + private clusterClient: IClusterClient, + log: Logger, + private licensing: LicensingPluginSetup ) { + this.log = log.get('reindex_worker'); if (ReindexWorker.workerSingleton) { throw new Error(`More than one ReindexWorker cannot be created.`); } + const callAsInternalUser = this.clusterClient.callAsInternalUser.bind(this.clusterClient); + this.reindexService = reindexServiceFactory( - this.callWithInternalUser, - this.xpackInfo, - reindexActionsFactory(this.client, this.callWithInternalUser), - this.log + callAsInternalUser, + reindexActionsFactory(this.client, callAsInternalUser), + log, + this.licensing ); ReindexWorker.workerSingleton = this; @@ -68,7 +67,7 @@ export class ReindexWorker { * Begins loop (1) to begin checking for in progress reindex operations. */ public start = () => { - this.log(['debug', ...LOG_TAGS], `Starting worker...`); + this.log.debug('Starting worker...'); this.continuePolling = true; this.pollForOperations(); }; @@ -77,7 +76,7 @@ export class ReindexWorker { * Stops the worker from processing any further reindex operations. */ public stop = () => { - this.log(['debug', ...LOG_TAGS], `Stopping worker...`); + this.log.debug('Stopping worker...'); if (this.timeout) { clearTimeout(this.timeout); } @@ -107,7 +106,7 @@ export class ReindexWorker { this.updateOperationLoopRunning = true; while (this.inProgressOps.length > 0) { - this.log(['debug', ...LOG_TAGS], `Updating ${this.inProgressOps.length} reindex operations`); + this.log.debug(`Updating ${this.inProgressOps.length} reindex operations`); // Push each operation through the state machine and refresh. await Promise.all(this.inProgressOps.map(this.processNextStep)); @@ -118,7 +117,7 @@ export class ReindexWorker { }; private pollForOperations = async () => { - this.log(['debug', ...LOG_TAGS], `Polling for reindex operations`); + this.log.debug(`Polling for reindex operations`); await this.refresh(); @@ -131,7 +130,7 @@ export class ReindexWorker { try { this.inProgressOps = await this.reindexService.findAllByStatus(ReindexStatus.inProgress); } catch (e) { - this.log(['debug', ...LOG_TAGS], `Could not fetch riendex operations from Elasticsearch`); + this.log.debug(`Could not fetch reindex operations from Elasticsearch`); this.inProgressOps = []; } @@ -159,10 +158,13 @@ export class ReindexWorker { } // Setup a ReindexService specific to these credentials. - const fakeRequest = { headers: credential } as Request; - const callCluster = this.callWithRequest.bind(null, fakeRequest) as CallCluster; - const actions = reindexActionsFactory(this.client, callCluster); - const service = reindexServiceFactory(callCluster, this.xpackInfo, actions, this.log); + const fakeRequest: FakeRequest = { headers: credential }; + + const scopedClusterClient = this.clusterClient.asScoped(fakeRequest); + const callAsCurrentUser = scopedClusterClient.callAsCurrentUser.bind(scopedClusterClient); + const actions = reindexActionsFactory(this.client, callAsCurrentUser); + + const service = reindexServiceFactory(callAsCurrentUser, actions, this.log, this.licensing); reindexOp = await swallowExceptions(service.processNextStep, this.log)(reindexOp); // Update credential store with most recent state. @@ -176,18 +178,15 @@ export class ReindexWorker { */ const swallowExceptions = ( func: (reindexOp: ReindexSavedObject) => Promise, - log: Server['log'] + log: Logger ) => async (reindexOp: ReindexSavedObject) => { try { return await func(reindexOp); } catch (e) { if (reindexOp.attributes.locked) { - log(['debug', ...LOG_TAGS], `Skipping reindexOp with unexpired lock: ${reindexOp.id}`); + log.debug(`Skipping reindexOp with unexpired lock: ${reindexOp.id}`); } else { - log( - ['warning', ...LOG_TAGS], - `Error when trying to process reindexOp (${reindexOp.id}): ${e.toString()}` - ); + log.warn(`Error when trying to process reindexOp (${reindexOp.id}): ${e.toString()}`); } return reindexOp; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts similarity index 51% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts index 5f95f6e9fd555..703351c45ba5a 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../../common/types'; import { upsertUIOpenOption } from './es_ui_open_apis'; /** @@ -13,54 +14,29 @@ import { upsertUIOpenOption } from './es_ui_open_apis'; * more thoroughly in the lib/telemetry tests. */ describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { - const mockIncrementCounter = jest.fn(); - const server = jest.fn().mockReturnValue({ - savedObjects: { - getSavedObjectsRepository: jest.fn().mockImplementation(() => { - return { - incrementCounter: mockIncrementCounter, - }; - }), - }, - plugins: { - elasticsearch: { - getCluster: () => { - return { - callWithInternalUser: {}, - }; - }, - }, - }, - }); - - const request = jest.fn().mockReturnValue({ - payload: { - overview: true, - cluster: true, - indices: true, - }, - }); - describe('Upsert UIOpen Option', () => { it('call saved objects internal repository with the correct info', async () => { - const serverMock = server(); - const incCounterSORepoFunc = serverMock.savedObjects.getSavedObjectsRepository() - .incrementCounter; + const internalRepo = savedObjectsRepositoryMock.create(); - await upsertUIOpenOption(serverMock, request()); + await upsertUIOpenOption({ + overview: true, + cluster: true, + indices: true, + savedObjects: { createInternalRepository: () => internalRepo } as any, + }); - expect(incCounterSORepoFunc).toHaveBeenCalledTimes(3); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3); + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_open.overview` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_open.cluster` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_open.indices` diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts new file mode 100644 index 0000000000000..64e9b0f217555 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts @@ -0,0 +1,58 @@ +/* + * 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 { SavedObjectsServiceStart } from 'src/core/server'; +import { + UIOpen, + UIOpenOption, + UPGRADE_ASSISTANT_DOC_ID, + UPGRADE_ASSISTANT_TYPE, +} from '../../../common/types'; + +interface IncrementUIOpenDependencies { + uiOpenOptionCounter: UIOpenOption; + savedObjects: SavedObjectsServiceStart; +} + +async function incrementUIOpenOptionCounter({ + savedObjects, + uiOpenOptionCounter, +}: IncrementUIOpenDependencies) { + const internalRepository = savedObjects.createInternalRepository(); + + await internalRepository.incrementCounter( + UPGRADE_ASSISTANT_TYPE, + UPGRADE_ASSISTANT_DOC_ID, + `ui_open.${uiOpenOptionCounter}` + ); +} + +type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; + +export async function upsertUIOpenOption({ + overview, + cluster, + indices, + savedObjects, +}: UpsertUIOpenOptionDependencies): Promise { + if (overview) { + await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' }); + } + + if (cluster) { + await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'cluster' }); + } + + if (indices) { + await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'indices' }); + } + + return { + overview, + cluster, + indices, + }; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts similarity index 52% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts index 3f2c80f7d6b75..31e4e3f07b5de 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../../common/types'; +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; import { upsertUIReindexOption } from './es_ui_reindex_apis'; /** @@ -13,60 +13,34 @@ import { upsertUIReindexOption } from './es_ui_reindex_apis'; * more thoroughly in the lib/telemetry tests. */ describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { - const mockIncrementCounter = jest.fn(); - const server = jest.fn().mockReturnValue({ - savedObjects: { - getSavedObjectsRepository: jest.fn().mockImplementation(() => { - return { - incrementCounter: mockIncrementCounter, - }; - }), - }, - plugins: { - elasticsearch: { - getCluster: () => { - return { - callWithInternalUser: {}, - }; - }, - }, - }, - }); - - const request = jest.fn().mockReturnValue({ - payload: { - close: true, - open: true, - start: true, - stop: true, - }, - }); - describe('Upsert UIReindex Option', () => { it('call saved objects internal repository with the correct info', async () => { - const serverMock = server(); - const incCounterSORepoFunc = serverMock.savedObjects.getSavedObjectsRepository() - .incrementCounter; - - await upsertUIReindexOption(serverMock, request()); + const internalRepo = savedObjectsRepositoryMock.create(); + await upsertUIReindexOption({ + close: true, + open: true, + start: true, + stop: true, + savedObjects: { createInternalRepository: () => internalRepo } as any, + }); - expect(incCounterSORepoFunc).toHaveBeenCalledTimes(4); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4); + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.close` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.open` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.start` ); - expect(incCounterSORepoFunc).toHaveBeenCalledWith( + expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, `ui_reindex.stop` diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts new file mode 100644 index 0000000000000..0aaaf63196d67 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts @@ -0,0 +1,64 @@ +/* + * 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 { SavedObjectsServiceStart } from 'src/core/server'; +import { + UIReindex, + UIReindexOption, + UPGRADE_ASSISTANT_DOC_ID, + UPGRADE_ASSISTANT_TYPE, +} from '../../../common/types'; + +interface IncrementUIReindexOptionDependencies { + uiReindexOptionCounter: UIReindexOption; + savedObjects: SavedObjectsServiceStart; +} + +async function incrementUIReindexOptionCounter({ + savedObjects, + uiReindexOptionCounter, +}: IncrementUIReindexOptionDependencies) { + const internalRepository = savedObjects.createInternalRepository(); + + await internalRepository.incrementCounter( + UPGRADE_ASSISTANT_TYPE, + UPGRADE_ASSISTANT_DOC_ID, + `ui_reindex.${uiReindexOptionCounter}` + ); +} + +type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; + +export async function upsertUIReindexOption({ + start, + close, + open, + stop, + savedObjects, +}: UpsertUIReindexOptionDepencies): Promise { + if (close) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'close' }); + } + + if (open) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'open' }); + } + + if (start) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'start' }); + } + + if (stop) { + await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'stop' }); + } + + return { + close, + open, + start, + stop, + }; +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/index.ts diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts similarity index 78% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 27a0eef0d16f6..a4833d9a3d7fe 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { registerUpgradeAssistantUsageCollector } from './usage_collector'; +import { IClusterClient } from 'src/core/server'; /** * Since these route callbacks are so thin, these serve simply as integration tests @@ -14,20 +15,31 @@ import { registerUpgradeAssistantUsageCollector } from './usage_collector'; describe('Upgrade Assistant Usage Collector', () => { let makeUsageCollectorStub: any; let registerStub: any; - let server: any; + let dependencies: any; let callClusterStub: any; let usageCollection: any; + let clusterClient: IClusterClient; beforeEach(() => { + clusterClient = elasticsearchServiceMock.createClusterClient(); + (clusterClient.callAsInternalUser as jest.Mock).mockResolvedValue({ + persistent: {}, + transient: { + logger: { + deprecation: 'WARN', + }, + }, + }); makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); usageCollection = { makeUsageCollector: makeUsageCollectorStub, registerCollector: registerStub, }; - server = jest.fn().mockReturnValue({ + dependencies = { + usageCollection, savedObjects: { - getSavedObjectsRepository: jest.fn().mockImplementation(() => { + createInternalRepository: jest.fn().mockImplementation(() => { return { get: () => { return { @@ -45,31 +57,26 @@ describe('Upgrade Assistant Usage Collector', () => { }; }), }, - }); - callClusterStub = jest.fn().mockResolvedValue({ - persistent: {}, - transient: { - logger: { - deprecation: 'WARN', - }, + elasticsearch: { + adminClient: clusterClient, }, - }); + }; }); describe('registerUpgradeAssistantUsageCollector', () => { it('should registerCollector', () => { - registerUpgradeAssistantUsageCollector(usageCollection, server()); + registerUpgradeAssistantUsageCollector(dependencies); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = upgrade-assistant', () => { - registerUpgradeAssistantUsageCollector(usageCollection, server()); + registerUpgradeAssistantUsageCollector(dependencies); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('upgrade-assistant-telemetry'); }); it('fetchUpgradeAssistantMetrics should return correct info', async () => { - registerUpgradeAssistantUsageCollector(usageCollection, server()); + registerUpgradeAssistantUsageCollector(dependencies); const upgradeAssistantStats = await makeUsageCollectorStub.mock.calls[0][0].fetch( callClusterStub ); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts similarity index 72% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts rename to x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 1d24d190fa9f2..79d6e53c64ec0 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -5,8 +5,12 @@ */ import { set } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsRepository } from 'src/core/server/saved_objects/service/lib/repository'; +import { + APICaller, + ElasticsearchServiceSetup, + ISavedObjectsRepository, + SavedObjectsServiceStart, +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { UPGRADE_ASSISTANT_DOC_ID, @@ -14,12 +18,11 @@ import { UpgradeAssistantTelemetry, UpgradeAssistantTelemetrySavedObject, UpgradeAssistantTelemetrySavedObjectAttributes, -} from '../../../../common/types'; -import { ServerShim } from '../../types'; +} from '../../../common/types'; import { isDeprecationLoggingEnabled } from '../es_deprecation_logging_apis'; async function getSavedObjectAttributesFromRepo( - savedObjectsRepository: SavedObjectsRepository, + savedObjectsRepository: ISavedObjectsRepository, docType: string, docID: string ) { @@ -35,9 +38,9 @@ async function getSavedObjectAttributesFromRepo( } } -async function getDeprecationLoggingStatusValue(callCluster: any): Promise { +async function getDeprecationLoggingStatusValue(callAsCurrentUser: APICaller): Promise { try { - const loggerDeprecationCallResult = await callCluster('cluster.getSettings', { + const loggerDeprecationCallResult = await callAsCurrentUser('cluster.getSettings', { includeDefaults: true, }); @@ -48,17 +51,17 @@ async function getDeprecationLoggingStatusValue(callCluster: any): Promise { - const { getSavedObjectsRepository } = server.savedObjects; - const savedObjectsRepository = getSavedObjectsRepository(callCluster); + const savedObjectsRepository = savedObjects.createInternalRepository(); const upgradeAssistantSOAttributes = await getSavedObjectAttributesFromRepo( savedObjectsRepository, UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID ); - const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue(callCluster); + const callAsInternalUser = adminClient.callAsInternalUser.bind(adminClient); + const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue(callAsInternalUser); const getTelemetrySavedObject = ( upgradeAssistantTelemetrySavedObjectAttrs: UpgradeAssistantTelemetrySavedObjectAttributes | null @@ -103,14 +106,21 @@ export async function fetchUpgradeAssistantMetrics( }; } -export function registerUpgradeAssistantUsageCollector( - usageCollection: UsageCollectionSetup, - server: ServerShim -) { +interface Dependencies { + elasticsearch: ElasticsearchServiceSetup; + savedObjects: SavedObjectsServiceStart; + usageCollection: UsageCollectionSetup; +} + +export function registerUpgradeAssistantUsageCollector({ + elasticsearch, + usageCollection, + savedObjects, +}: Dependencies) { const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ type: UPGRADE_ASSISTANT_TYPE, isReady: () => true, - fetch: async (callCluster: any) => fetchUpgradeAssistantMetrics(callCluster, server), + fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), }); usageCollection.registerCollector(upgradeAssistantUsageCollector); diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts new file mode 100644 index 0000000000000..6ccd073a9e020 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -0,0 +1,125 @@ +/* + * 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +import { + Plugin, + CoreSetup, + CoreStart, + PluginInitializerContext, + Logger, + ElasticsearchServiceSetup, + SavedObjectsClient, + SavedObjectsServiceStart, +} from '../../../../src/core/server'; + +import { CloudSetup } from '../../cloud/server'; +import { LicensingPluginSetup } from '../../licensing/server'; + +import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store'; +import { ReindexWorker } from './lib/reindexing'; +import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; +import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; +import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; +import { registerReindexIndicesRoutes, createReindexWorker } from './routes/reindex_indices'; +import { registerTelemetryRoutes } from './routes/telemetry'; +import { RouteDependencies } from './types'; + +interface PluginsSetup { + usageCollection: UsageCollectionSetup; + licensing: LicensingPluginSetup; + cloud?: CloudSetup; +} + +export class UpgradeAssistantServerPlugin implements Plugin { + private readonly logger: Logger; + private readonly credentialStore: CredentialStore; + + // Properties set at setup + private licensing?: LicensingPluginSetup; + private elasticSearchService?: ElasticsearchServiceSetup; + + // Properties set at start + private savedObjectsServiceStart?: SavedObjectsServiceStart; + private worker?: ReindexWorker; + + constructor({ logger }: PluginInitializerContext) { + this.logger = logger.get(); + this.credentialStore = credentialStoreFactory(); + } + + private getWorker() { + if (!this.worker) { + throw new Error('Worker unavailable'); + } + return this.worker; + } + + setup( + { http, elasticsearch, getStartServices, capabilities }: CoreSetup, + { usageCollection, cloud, licensing }: PluginsSetup + ) { + this.elasticSearchService = elasticsearch; + this.licensing = licensing; + + const router = http.createRouter(); + + const dependencies: RouteDependencies = { + cloud, + router, + credentialStore: this.credentialStore, + log: this.logger, + getSavedObjectsService: () => { + if (!this.savedObjectsServiceStart) { + throw new Error('Saved Objects Start service not available'); + } + return this.savedObjectsServiceStart; + }, + licensing, + }; + + registerClusterCheckupRoutes(dependencies); + registerDeprecationLoggingRoutes(dependencies); + registerReindexIndicesRoutes(dependencies, this.getWorker.bind(this)); + // Bootstrap the needed routes and the collector for the telemetry + registerTelemetryRoutes(dependencies); + + if (usageCollection) { + getStartServices().then(([{ savedObjects }]) => { + registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, savedObjects }); + }); + } + } + + start({ savedObjects }: CoreStart) { + this.savedObjectsServiceStart = savedObjects; + + // The ReindexWorker uses a map of request headers that contain the authentication credentials + // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not + // want to expose these credentials to any unauthenticated users. We also want to avoid any need + // to add a user for a special index just for upgrading. This in-memory cache allows us to + // process jobs without the browser staying on the page, but will require that jobs go into + // a paused state if no Kibana nodes have the required credentials. + + this.worker = createReindexWorker({ + credentialStore: this.credentialStore, + licensing: this.licensing!, + elasticsearchService: this.elasticSearchService!, + logger: this.logger, + savedObjects: new SavedObjectsClient( + this.savedObjectsServiceStart.createInternalRepository() + ), + }); + + this.worker.start(); + } + + stop(): void { + if (this.worker) { + this.worker.stop(); + } + } +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts similarity index 92% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts rename to x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts index d09a66dbb4326..fb68e188bb255 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/request.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts @@ -6,7 +6,7 @@ export const createRequestMock = (opts?: { headers?: any; params?: Record; - payload?: Record; + body?: Record; }) => { return Object.assign({ headers: {} }, opts || {}); }; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts similarity index 72% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts rename to x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts index 3769bc389123e..a8be171dccd98 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/__mocks__/routes.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts @@ -3,7 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { RequestHandler, RequestHandlerContext } from 'src/core/server'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../src/core/server/mocks'; + +export const routeHandlerContextMock = ({ + core: { + elasticsearch: { + adminClient: elasticsearchServiceMock.createScopedClusterClient(), + dataClient: elasticsearchServiceMock.createScopedClusterClient(), + }, + savedObjects: { client: savedObjectsClientMock.create() }, + }, +} as unknown) as RequestHandlerContext; /** * Creates a very crude mock of the new platform router implementation. This enables use to test diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts similarity index 73% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts index 3fe2e1797182b..16f8001f8e1de 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/cluster_checkup.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; jest.mock('../lib/es_version_precheck', () => ({ @@ -24,32 +24,22 @@ import { registerClusterCheckupRoutes } from './cluster_checkup'; * more thoroughly in the es_migration_apis test. */ describe('cluster checkup API', () => { - afterEach(() => jest.clearAllMocks()); - let mockRouter: MockRouter; - let serverShim: any; - let ctxMock: any; - let mockPluginsSetup: any; + let routeDependencies: any; beforeEach(() => { mockRouter = createMockRouter(); - mockPluginsSetup = { + routeDependencies = { cloud: { isCloudEnabled: true, }, - }; - ctxMock = { - core: {}, - }; - serverShim = { router: mockRouter, - plugins: { - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - }, }; - registerClusterCheckupRoutes(serverShim, mockPluginsSetup); + registerClusterCheckupRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); }); describe('with cloud enabled', () => { @@ -62,11 +52,11 @@ describe('cluster checkup API', () => { nodes: [], }); - await serverShim.router.getHandler({ + await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); - expect(spy.mock.calls[0][2]).toBe(true); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + expect(spy.mock.calls[0][1]).toBe(true); }); }); @@ -77,10 +67,10 @@ describe('cluster checkup API', () => { indices: [], nodes: [], }); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(200); expect(JSON.stringify(resp.payload)).toMatchInlineSnapshot( @@ -93,10 +83,10 @@ describe('cluster checkup API', () => { e.status = 403; MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(e); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(403); }); @@ -104,10 +94,10 @@ describe('cluster checkup API', () => { it('returns an 500 error if it throws', async () => { MigrationApis.getUpgradeAssistantStatus.mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/status', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(500); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts new file mode 100644 index 0000000000000..22a121ab78683 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_checkup.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getUpgradeAssistantStatus } from '../lib/es_migration_apis'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerClusterCheckupRoutes({ cloud, router }: RouteDependencies) { + const isCloudEnabled = Boolean(cloud?.isCloudEnabled); + + router.get( + { + path: '/api/upgrade_assistant/status', + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + try { + return response.ok({ + body: await getUpgradeAssistantStatus(dataClient, isCloudEnabled), + }); + } catch (e) { + if (e.status === 403) { + return response.forbidden(e.message); + } + + return response.internalError({ body: e }); + } + } + ) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts similarity index 56% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts index d663361956374..845a0238f7918 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/deprecation_logging.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts @@ -5,7 +5,7 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; jest.mock('../lib/es_version_precheck', () => ({ @@ -21,42 +21,42 @@ import { registerDeprecationLoggingRoutes } from './deprecation_logging'; */ describe('deprecation logging API', () => { let mockRouter: MockRouter; - let serverShim: any; - let callWithRequest: any; - const ctxMock: any = {}; + let routeDependencies: any; beforeEach(() => { mockRouter = createMockRouter(); - callWithRequest = jest.fn(); - serverShim = { + routeDependencies = { router: mockRouter, - plugins: { - elasticsearch: { - getCluster: () => ({ callWithRequest } as any), - } as any, - }, }; - registerDeprecationLoggingRoutes(serverShim); + registerDeprecationLoggingRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); }); describe('GET /api/upgrade_assistant/deprecation_logging', () => { it('returns isEnabled', async () => { - callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'WARN' } } }); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockResolvedValue({ + default: { logger: { deprecation: 'WARN' } }, + }); + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(200); expect(resp.payload).toEqual({ isEnabled: true }); }); it('returns an error if it throws', async () => { - callWithRequest.mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.status).toEqual(500); }); @@ -64,21 +64,25 @@ describe('deprecation logging API', () => { describe('PUT /api/upgrade_assistant/deprecation_logging', () => { it('returns isEnabled', async () => { - callWithRequest.mockResolvedValue({ default: { logger: { deprecation: 'ERROR' } } }); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockResolvedValue({ + default: { logger: { deprecation: 'ERROR' } }, + }); + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); expect(resp.payload).toEqual({ isEnabled: false }); }); it('returns an error if it throws', async () => { - callWithRequest.mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + (routeHandlerContextMock.core.elasticsearch.dataClient + .callAsCurrentUser as jest.Mock).mockRejectedValue(new Error(`scary error!`)); + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/deprecation_logging', - })(ctxMock, { body: { isEnabled: false } }, kibanaResponseFactory); + })(routeHandlerContextMock, { body: { isEnabled: false } }, kibanaResponseFactory); expect(resp.status).toEqual(500); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts new file mode 100644 index 0000000000000..739a789c95ce0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts @@ -0,0 +1,72 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { + getDeprecationLoggingStatus, + setDeprecationLogging, +} from '../lib/es_deprecation_logging_apis'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) { + router.get( + { + path: '/api/upgrade_assistant/deprecation_logging', + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + try { + const result = await getDeprecationLoggingStatus(dataClient); + return response.ok({ body: result }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ) + ); + + router.put( + { + path: '/api/upgrade_assistant/deprecation_logging', + validate: { + body: schema.object({ + isEnabled: schema.boolean(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + try { + const { isEnabled } = request.body as { isEnabled: boolean }; + return response.ok({ + body: await setDeprecationLogging(dataClient, isEnabled), + }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts similarity index 80% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts index d520324239656..695bb6304cfdf 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/reindex_indices.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { licensingMock } from '../../../licensing/server/mocks'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; const mockReindexService = { @@ -31,12 +31,7 @@ jest.mock('../lib/reindexing', () => { }; }); -import { - IndexGroup, - ReindexSavedObject, - ReindexStatus, - ReindexWarning, -} from '../../../common/types'; +import { IndexGroup, ReindexSavedObject, ReindexStatus, ReindexWarning } from '../../common/types'; import { credentialStoreFactory } from '../lib/reindexing/credential_store'; import { registerReindexIndicesRoutes } from './reindex_indices'; @@ -46,9 +41,8 @@ import { registerReindexIndicesRoutes } from './reindex_indices'; * more thoroughly in the es_migration_apis test. */ describe('reindex API', () => { - let serverShim: any; + let routeDependencies: any; let mockRouter: MockRouter; - let ctxMock: any; const credentialStore = credentialStoreFactory(); const worker = { @@ -57,24 +51,13 @@ describe('reindex API', () => { } as any; beforeEach(() => { - ctxMock = { - core: { - savedObjects: savedObjectsClientMock.create(), - }, - }; mockRouter = createMockRouter(); - serverShim = { + routeDependencies = { + credentialStore, router: mockRouter, - plugins: { - xpack_main: { - info: jest.fn(), - }, - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - }, + licensing: licensingMock.createSetup(), }; - registerReindexIndicesRoutes(serverShim, worker, credentialStore); + registerReindexIndicesRoutes(routeDependencies, () => worker); mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); mockReindexService.detectReindexWarnings.mockReset(); @@ -92,7 +75,9 @@ describe('reindex API', () => { credentialStore.clear(); }); - afterEach(() => jest.clearAllMocks()); + afterEach(() => { + jest.resetAllMocks(); + }); describe('GET /api/upgrade_assistant/reindex/{indexName}', () => { it('returns the attributes of the reindex operation and reindex warnings', async () => { @@ -101,10 +86,14 @@ describe('reindex API', () => { }); mockReindexService.detectReindexWarnings.mockResolvedValueOnce([ReindexWarning.allField]); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'wowIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'wowIndex' } }), + kibanaResponseFactory + ); // It called into the service correctly expect(mockReindexService.findReindexOperation).toHaveBeenCalledWith('wowIndex'); @@ -121,10 +110,14 @@ describe('reindex API', () => { mockReindexService.findReindexOperation.mockResolvedValueOnce(null); mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'anIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'anIndex' } }), + kibanaResponseFactory + ); expect(resp.status).toEqual(200); const data = resp.payload; @@ -137,10 +130,14 @@ describe('reindex API', () => { mockReindexService.detectReindexWarnings.mockResolvedValueOnce([]); mockReindexService.getIndexGroup.mockReturnValue(IndexGroup.ml); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'get', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'anIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'anIndex' } }), + kibanaResponseFactory + ); expect(resp.status).toEqual(200); const data = resp.payload; @@ -154,10 +151,14 @@ describe('reindex API', () => { attributes: { indexName: 'theIndex' }, }); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'theIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'theIndex' } }), + kibanaResponseFactory + ); // It called create correctly expect(mockReindexService.createReindexOperation).toHaveBeenCalledWith('theIndex'); @@ -173,10 +174,14 @@ describe('reindex API', () => { attributes: { indexName: 'theIndex' }, }); - await serverShim.router.getHandler({ + await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', - })(ctxMock, createRequestMock({ params: { indexName: 'theIndex' } }), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ params: { indexName: 'theIndex' } }), + kibanaResponseFactory + ); expect(worker.forceRefresh).toHaveBeenCalled(); }); @@ -187,11 +192,11 @@ describe('reindex API', () => { } as ReindexSavedObject; mockReindexService.createReindexOperation.mockResolvedValueOnce(reindexOp); - await serverShim.router.getHandler({ + await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ headers: { 'kbn-auth-x': 'HERE!', @@ -212,11 +217,11 @@ describe('reindex API', () => { attributes: { indexName: 'theIndex', status: ReindexStatus.inProgress }, }); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ params: { indexName: 'theIndex' }, }), @@ -235,11 +240,11 @@ describe('reindex API', () => { it('returns a 403 if required privileges fails', async () => { mockReindexService.hasRequiredPrivileges.mockResolvedValueOnce(false); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ params: { indexName: 'theIndex' }, }), @@ -254,11 +259,11 @@ describe('reindex API', () => { it('returns a 501', async () => { mockReindexService.cancelReindexing.mockResolvedValueOnce({}); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'post', pathPattern: '/api/upgrade_assistant/reindex/{indexName}/cancel', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ params: { indexName: 'cancelMe' }, }), diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts new file mode 100644 index 0000000000000..a910145474061 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices.ts @@ -0,0 +1,217 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { Logger, ElasticsearchServiceSetup, SavedObjectsClient } from 'src/core/server'; +import { ReindexStatus } from '../../common/types'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { reindexServiceFactory, ReindexWorker } from '../lib/reindexing'; +import { CredentialStore } from '../lib/reindexing/credential_store'; +import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; +import { RouteDependencies } from '../types'; +import { LicensingPluginSetup } from '../../../licensing/server'; + +interface CreateReindexWorker { + logger: Logger; + elasticsearchService: ElasticsearchServiceSetup; + credentialStore: CredentialStore; + savedObjects: SavedObjectsClient; + licensing: LicensingPluginSetup; +} + +export function createReindexWorker({ + logger, + elasticsearchService, + credentialStore, + savedObjects, + licensing, +}: CreateReindexWorker) { + const { adminClient } = elasticsearchService; + return new ReindexWorker(savedObjects, credentialStore, adminClient, logger, licensing); +} + +export function registerReindexIndicesRoutes( + { credentialStore, router, licensing, log }: RouteDependencies, + getWorker: () => ReindexWorker +) { + const BASE_PATH = '/api/upgrade_assistant/reindex'; + + // Start reindex for an index + router.post( + { + path: `${BASE_PATH}/{indexName}`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects, + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + const { indexName } = request.params as any; + const { client } = savedObjects; + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + const reindexService = reindexServiceFactory( + callAsCurrentUser, + reindexActions, + log, + licensing + ); + + try { + if (!(await reindexService.hasRequiredPrivileges(indexName))) { + return response.forbidden({ + body: `You do not have adequate privileges to reindex this index.`, + }); + } + + const existingOp = await reindexService.findReindexOperation(indexName); + + // If the reindexOp already exists and it's paused, resume it. Otherwise create a new one. + const reindexOp = + existingOp && existingOp.attributes.status === ReindexStatus.paused + ? await reindexService.resumeReindexOperation(indexName) + : await reindexService.createReindexOperation(indexName); + + // Add users credentials for the worker to use + credentialStore.set(reindexOp, request.headers); + + // Kick the worker on this node to immediately pickup the new reindex operation. + getWorker().forceRefresh(); + + return response.ok({ body: reindexOp.attributes }); + } catch (e) { + return response.internalError({ body: e }); + } + } + ) + ); + + // Get status + router.get( + { + path: `${BASE_PATH}/{indexName}`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects, + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + const { client } = savedObjects; + const { indexName } = request.params as any; + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + const reindexService = reindexServiceFactory( + callAsCurrentUser, + reindexActions, + log, + licensing + ); + + try { + const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName); + const reindexOp = await reindexService.findReindexOperation(indexName); + // If the user doesn't have privileges than querying for warnings is going to fail. + const warnings = hasRequiredPrivileges + ? await reindexService.detectReindexWarnings(indexName) + : []; + const indexGroup = reindexService.getIndexGroup(indexName); + + return response.ok({ + body: { + reindexOp: reindexOp ? reindexOp.attributes : null, + warnings, + indexGroup, + hasRequiredPrivileges, + }, + }); + } catch (e) { + if (!e.isBoom) { + return response.internalError({ body: e }); + } + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.statusCode, + }); + } + } + ) + ); + + // Cancel reindex + router.post( + { + path: `${BASE_PATH}/{indexName}/cancel`, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects, + elasticsearch: { dataClient }, + }, + }, + request, + response + ) => { + const { indexName } = request.params as any; + const { client } = savedObjects; + const callAsCurrentUser = dataClient.callAsCurrentUser.bind(dataClient); + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + const reindexService = reindexServiceFactory( + callAsCurrentUser, + reindexActions, + log, + licensing + ); + + try { + await reindexService.cancelReindexing(indexName); + + return response.ok({ body: { acknowledged: true } }); + } catch (e) { + if (!e.isBoom) { + return response.internalError({ body: e }); + } + return response.customError({ + body: { + message: e.message, + }, + statusCode: e.statusCode, + }); + } + } + ) + ); +} diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts similarity index 79% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts rename to x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index 582c75e3701b6..b2b8ccf1ca57a 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -5,7 +5,8 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { createMockRouter, MockRouter } from './__mocks__/routes.mock'; +import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ @@ -26,24 +27,15 @@ import { registerTelemetryRoutes } from './telemetry'; * more thoroughly in the lib/telemetry tests. */ describe('Upgrade Assistant Telemetry API', () => { - let serverShim: any; + let routeDependencies: any; let mockRouter: MockRouter; - let ctxMock: any; beforeEach(() => { - ctxMock = {}; mockRouter = createMockRouter(); - serverShim = { + routeDependencies = { + getSavedObjectsService: () => savedObjectsServiceMock.create(), router: mockRouter, - plugins: { - xpack_main: { - info: jest.fn(), - }, - elasticsearch: { - getCluster: () => ({ callWithRequest: jest.fn() } as any), - } as any, - }, }; - registerTelemetryRoutes(serverShim); + registerTelemetryRoutes(routeDependencies); }); afterEach(() => jest.clearAllMocks()); @@ -57,10 +49,14 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_open', - })(ctxMock, createRequestMock(), kibanaResponseFactory); + })( + routeHandlerContextMock, + createRequestMock({ body: returnPayload }), + kibanaResponseFactory + ); expect(resp.payload).toEqual(returnPayload); }); @@ -74,13 +70,13 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_open', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { overview: true, cluster: true, indices: true, @@ -95,13 +91,13 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_open', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { overview: false, }, }), @@ -123,13 +119,13 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { overview: false, }, }), @@ -149,13 +145,13 @@ describe('Upgrade Assistant Telemetry API', () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(returnPayload); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { close: true, open: true, start: true, @@ -171,13 +167,13 @@ describe('Upgrade Assistant Telemetry API', () => { it('returns an error if it throws', async () => { (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - const resp = await serverShim.router.getHandler({ + const resp = await routeDependencies.router.getHandler({ method: 'put', pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', })( - ctxMock, + routeHandlerContextMock, createRequestMock({ - payload: { + body: { start: false, }, }), diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts similarity index 66% rename from x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts rename to x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts index f08c49809033d..900a5e64c55c3 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/routes/telemetry.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts @@ -7,11 +7,10 @@ import { schema } from '@kbn/config-schema'; import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { ServerShimWithRouter } from '../types'; -import { createRequestShim } from './create_request_shim'; +import { RouteDependencies } from '../types'; -export function registerTelemetryRoutes(server: ServerShimWithRouter) { - server.router.put( +export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { + router.put( { path: '/api/upgrade_assistant/telemetry/ui_open', validate: { @@ -23,16 +22,23 @@ export function registerTelemetryRoutes(server: ServerShimWithRouter) { }, }, async (ctx, request, response) => { - const reqShim = createRequestShim(request); + const { cluster, indices, overview } = request.body; try { - return response.ok({ body: await upsertUIOpenOption(server, reqShim) }); + return response.ok({ + body: await upsertUIOpenOption({ + savedObjects: getSavedObjectsService(), + cluster, + indices, + overview, + }), + }); } catch (e) { return response.internalError({ body: e }); } } ); - server.router.put( + router.put( { path: '/api/upgrade_assistant/telemetry/ui_reindex', validate: { @@ -45,9 +51,17 @@ export function registerTelemetryRoutes(server: ServerShimWithRouter) { }, }, async (ctx, request, response) => { - const reqShim = createRequestShim(request); + const { close, open, start, stop } = request.body; try { - return response.ok({ body: await upsertUIReindexOption(server, reqShim) }); + return response.ok({ + body: await upsertUIReindexOption({ + savedObjects: getSavedObjectsService(), + close, + open, + start, + stop, + }), + }); } catch (e) { return response.internalError({ body: e }); } diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts new file mode 100644 index 0000000000000..3f3beadd2f333 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, Logger, SavedObjectsServiceStart } from 'src/core/server'; +import { CloudSetup } from '../../cloud/server'; +import { CredentialStore } from './lib/reindexing/credential_store'; +import { LicensingPluginSetup } from '../../licensing/server'; + +export interface RouteDependencies { + router: IRouter; + credentialStore: CredentialStore; + log: Logger; + getSavedObjectsService: () => SavedObjectsServiceStart; + licensing: LicensingPluginSetup; + cloud?: CloudSetup; +} diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json new file mode 100644 index 0000000000000..dd61716325afc --- /dev/null +++ b/x-pack/plugins/uptime/kibana.json @@ -0,0 +1,9 @@ +{ + "configPath": ["xpack"], + "id": "uptime", + "kibanaVersion": "kibana", + "requiredPlugins": ["features", "licensing", "usageCollection"], + "server": true, + "ui": false, + "version": "8.0.0" +} diff --git a/x-pack/legacy/plugins/uptime/server/graphql/constants.ts b/x-pack/plugins/uptime/server/graphql/constants.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/constants.ts rename to x-pack/plugins/uptime/server/graphql/constants.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/index.ts b/x-pack/plugins/uptime/server/graphql/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/index.ts rename to x-pack/plugins/uptime/server/graphql/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/index.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitor_states/index.ts rename to x-pack/plugins/uptime/server/graphql/monitor_states/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts similarity index 89% rename from x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts index e2b076d570843..6ac42f7717259 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts @@ -6,13 +6,13 @@ import { CreateUMGraphQLResolvers, UMContext } from '../types'; import { UMServerLibs } from '../../lib/lib'; -import { UMResolver } from '../../../common/graphql/resolver_types'; +import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; import { GetMonitorStatesQueryArgs, MonitorSummaryResult, StatesIndexStatus, -} from '../../../common/graphql/types'; -import { CONTEXT_DEFAULTS } from '../../../common/constants/context_defaults'; +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants/context_defaults'; export type UMGetMonitorStatesResolver = UMResolver< MonitorSummaryResult | Promise, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitor_states/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/monitor_states/schema.gql.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/index.ts b/x-pack/plugins/uptime/server/graphql/monitors/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitors/index.ts rename to x-pack/plugins/uptime/server/graphql/monitors/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/plugins/uptime/server/graphql/monitors/resolvers.ts similarity index 71% rename from x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/monitors/resolvers.ts index 19f23fa1bb934..b39c5f42bfd75 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMResolver } from '../../../common/graphql/resolver_types'; -import { GetMonitorChartsDataQueryArgs, MonitorChart } from '../../../common/graphql/types'; +import { UMGqlRange } from '../../../../../legacy/plugins/uptime/common/domain_types'; +import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; +import { + GetMonitorChartsDataQueryArgs, + MonitorChart, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { CreateUMGraphQLResolvers, UMContext } from '../types'; +export type UMMonitorsResolver = UMResolver, any, UMGqlRange, UMContext>; + export type UMGetMonitorChartsResolver = UMResolver< any | Promise, any, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/monitors/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/monitors/schema.gql.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/pings/index.ts b/x-pack/plugins/uptime/server/graphql/pings/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/pings/index.ts rename to x-pack/plugins/uptime/server/graphql/pings/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts b/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts similarity index 84% rename from x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/pings/resolvers.ts index de83a9ced16b2..b383fc5d5fb15 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UMResolver } from '../../../common/graphql/resolver_types'; -import { AllPingsQueryArgs, PingResults } from '../../../common/graphql/types'; +import { UMResolver } from '../../../../../legacy/plugins/uptime/common/graphql/resolver_types'; +import { + AllPingsQueryArgs, + PingResults, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { UMContext } from '../types'; import { CreateUMGraphQLResolvers } from '../types'; diff --git a/x-pack/legacy/plugins/uptime/server/graphql/pings/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/pings/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/pings/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/pings/schema.gql.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/types.ts b/x-pack/plugins/uptime/server/graphql/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/types.ts rename to x-pack/plugins/uptime/server/graphql/types.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_literal.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/parse_value.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/__tests__/serialize.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/resolvers.ts diff --git a/x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts b/x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts rename to x-pack/plugins/uptime/server/graphql/unsigned_int_scalar/schema.gql.ts diff --git a/x-pack/plugins/uptime/server/index.ts b/x-pack/plugins/uptime/server/index.ts new file mode 100644 index 0000000000000..bec47fa9db4cf --- /dev/null +++ b/x-pack/plugins/uptime/server/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. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; +import { Plugin } from './plugin'; + +export { initServerWithKibana, KibanaServer } from './kibana.index'; +export const plugin = (initializerContext: PluginInitializerContext) => + new Plugin(initializerContext); diff --git a/x-pack/legacy/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts similarity index 82% rename from x-pack/legacy/plugins/uptime/server/kibana.index.ts rename to x-pack/plugins/uptime/server/kibana.index.ts index 73fabc629946b..c7ac3a70c0494 100644 --- a/x-pack/legacy/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { Request, Server } from 'hapi'; -import { PLUGIN } from '../common/constants'; +import { PLUGIN } from '../../../legacy/plugins/uptime/common/constants'; import { KibanaTelemetryAdapter } from './lib/adapters/telemetry'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; @@ -25,17 +24,13 @@ export interface KibanaServer extends Server { } export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => { - const { usageCollection, xpack } = plugins; - const libs = compose(server, plugins); + const { features, usageCollection } = plugins; + const libs = compose(server); KibanaTelemetryAdapter.registerUsageCollector(usageCollection); - initUptimeServer(libs); - - xpack.registerFeature({ + features.registerFeature({ id: PLUGIN.ID, - name: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { - defaultMessage: 'Uptime', - }), + name: PLUGIN.NAME, navLinkId: PLUGIN.ID, icon: 'uptimeApp', app: ['uptime', 'kibana'], @@ -59,4 +54,6 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor }, }, }); + + initUptimeServer(libs); }; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts similarity index 85% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts rename to x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index fb2052bb4c87f..8dde6050d5d36 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -6,13 +6,9 @@ import { GraphQLSchema } from 'graphql'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { - SavedObjectsLegacyService, - IRouter, - CallAPIOptions, - SavedObjectsClientContract, -} from 'src/core/server'; +import { IRouter, CallAPIOptions, SavedObjectsClientContract } from 'src/core/server'; import { UMKibanaRoute } from '../../../rest_api'; +import { PluginSetupContract } from '../../../../../features/server'; type APICaller = ( endpoint: string, @@ -34,9 +30,8 @@ export interface UptimeCoreSetup { } export interface UptimeCorePlugins { - savedObjects: SavedObjectsLegacyService; + features: PluginSetupContract; usageCollection: UsageCollectionSetup; - xpack: any; } export interface UMBackendFrameworkAdapter { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/index.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/framework/index.ts rename to x-pack/plugins/uptime/server/lib/adapters/framework/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts rename to x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts b/x-pack/plugins/uptime/server/lib/adapters/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts rename to x-pack/plugins/uptime/server/lib/adapters/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/__snapshots__/kibana_telemetry_adapter.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/index.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/index.ts rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts rename to x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts b/x-pack/plugins/uptime/server/lib/compose/kibana.ts similarity index 80% rename from x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts rename to x-pack/plugins/uptime/server/lib/compose/kibana.ts index 875a5d9dc8c5c..edda5cb283323 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts +++ b/x-pack/plugins/uptime/server/lib/compose/kibana.ts @@ -8,9 +8,9 @@ import { UMKibanaBackendFrameworkAdapter } from '../adapters/framework'; import * as requests from '../requests'; import { licenseCheck } from '../domains'; import { UMDomainLibs, UMServerLibs } from '../lib'; -import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters/framework'; +import { UptimeCoreSetup } from '../adapters/framework'; -export function compose(server: UptimeCoreSetup, plugins: UptimeCorePlugins): UMServerLibs { +export function compose(server: UptimeCoreSetup): UMServerLibs { const framework = new UMKibanaBackendFrameworkAdapter(server); const domainLibs: UMDomainLibs = { diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap b/x-pack/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap rename to x-pack/plugins/uptime/server/lib/domains/__tests__/__snapshots__/license.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts b/x-pack/plugins/uptime/server/lib/domains/__tests__/license.test.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts rename to x-pack/plugins/uptime/server/lib/domains/__tests__/license.test.ts index 8c47b318da9bd..b842f55fc7579 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/__tests__/license.test.ts +++ b/x-pack/plugins/uptime/server/lib/domains/__tests__/license.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILicense } from '../../../../../../../plugins/licensing/server'; +import { ILicense } from '../../../../../licensing/server'; import { licenseCheck } from '../license'; describe('license check', () => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/index.ts b/x-pack/plugins/uptime/server/lib/domains/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/domains/index.ts rename to x-pack/plugins/uptime/server/lib/domains/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/domains/license.ts b/x-pack/plugins/uptime/server/lib/domains/license.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/domains/license.ts rename to x-pack/plugins/uptime/server/lib/domains/license.ts index b8b5722d79877..d272424379e48 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/domains/license.ts +++ b/x-pack/plugins/uptime/server/lib/domains/license.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILicense } from '../../../../../../plugins/licensing/server'; +import { ILicense } from '../../../../licensing/server'; export interface UMLicenseStatusResponse { statusCode: number; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap b/x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap rename to x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap b/x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap rename to x-pack/plugins/uptime/server/lib/helper/__test__/__snapshots__/get_filter_clause.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/get_filter_clause.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/get_histogram_interval_formatted.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts b/x-pack/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts rename to x-pack/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts b/x-pack/plugins/uptime/server/lib/helper/assert_close_to.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts rename to x-pack/plugins/uptime/server/lib/helper/assert_close_to.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts b/x-pack/plugins/uptime/server/lib/helper/get_filter_clause.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/get_filter_clause.ts rename to x-pack/plugins/uptime/server/lib/helper/get_filter_clause.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts rename to x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts index 26515fb4b4c63..fb44f5727aab3 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -5,7 +5,7 @@ */ import DateMath from '@elastic/datemath'; -import { QUERY } from '../../../common/constants'; +import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; export const parseRelativeDate = (dateStr: string, options = {}) => { // We need this this parsing because if user selects This week or this date diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts rename to x-pack/plugins/uptime/server/lib/helper/get_histogram_interval_formatted.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts b/x-pack/plugins/uptime/server/lib/helper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/index.ts rename to x-pack/plugins/uptime/server/lib/helper/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/make_date_rate_filter.ts b/x-pack/plugins/uptime/server/lib/helper/make_date_rate_filter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/make_date_rate_filter.ts rename to x-pack/plugins/uptime/server/lib/helper/make_date_rate_filter.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/object_to_array.ts b/x-pack/plugins/uptime/server/lib/helper/object_to_array.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/helper/object_to_array.ts rename to x-pack/plugins/uptime/server/lib/helper/object_to_array.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/lib.ts rename to x-pack/plugins/uptime/server/lib/lib.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap b/x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap rename to x-pack/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts rename to x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json b/x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json rename to x-pack/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/generate_filter_aggs.ts b/x-pack/plugins/uptime/server/lib/requests/generate_filter_aggs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/generate_filter_aggs.ts rename to x-pack/plugins/uptime/server/lib/requests/generate_filter_aggs.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts rename to x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts index 79259afe2b9eb..affe205a46844 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -5,9 +5,9 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { OverviewFilters } from '../../../common/runtime_types'; +import { OverviewFilters } from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { generateFilterAggs } from './generate_filter_aggs'; -import { INDEX_NAMES } from '../../../common/constants'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetFilterBarParams { /** @param dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts similarity index 92% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts rename to x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts index 4b40f800b6779..1ba1eb62e8439 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -6,8 +6,8 @@ import { APICaller } from 'src/core/server'; import { UMElasticsearchQueryFn } from '../adapters'; -import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../../src/plugins/data/server'; -import { INDEX_NAMES } from '../../../common/constants'; +import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export const getUptimeIndexPattern: UMElasticsearchQueryFn = async callES => { const indexPatternsFetcher = new IndexPatternsFetcher((...rest: Parameters) => diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts similarity index 76% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_index_status.ts rename to x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index e801b05d057f4..95aa7eeef88e1 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { StatesIndexStatus } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ callES }) => { const { diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts similarity index 91% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts rename to x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index bfaee3f2bf7ee..2d549fce06884 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { Ping } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetLatestMonitorParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor.ts similarity index 87% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor.ts index 94175616f374e..20103042f19ab 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { Ping } from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetMonitorParams { /** @member monitorId optional limit to monitorId */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_charts.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_charts.ts index b97cc7287e921..7dd17ef9aa80f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_charts.ts @@ -5,9 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { INDEX_NAMES } from '../../../common/constants'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; import { getHistogramIntervalFormatted } from '../helper'; -import { MonitorChart, LocationDurationLine } from '../../../common/graphql/types'; +import { + MonitorChart, + LocationDurationLine, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; export interface GetMonitorChartsParams { /** @member monitorId ID value for the selected monitor */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts similarity index 88% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index b516fde1ce844..eb3657e60a7bb 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -5,8 +5,11 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { + MonitorDetails, + MonitorError, +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetMonitorDetailsParams { monitorId: string; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts similarity index 92% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index e1a0e14fe951d..328ef54c404d3 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -5,8 +5,14 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { INDEX_NAMES, UNNAMED_LOCATION } from '../../../common/constants'; -import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; +import { + INDEX_NAMES, + UNNAMED_LOCATION, +} from '../../../../../legacy/plugins/uptime/common/constants'; +import { + MonitorLocations, + MonitorLocation, +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; /** * Fetch data for the monitor page title. diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts similarity index 89% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts rename to x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 32c82b1fa2098..5b02e2502a27e 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CONTEXT_DEFAULTS } from '../../../common/constants'; +import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; import { fetchPage } from './search'; import { UMElasticsearchQueryFn } from '../adapters'; -import { MonitorSummary, SortOrder, CursorDirection } from '../../../common/graphql/types'; +import { + MonitorSummary, + SortOrder, + CursorDirection, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { QueryContext } from './search'; export interface CursorPagination { diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts rename to x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 7b8ca4708255c..339409b63a4f6 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -5,10 +5,10 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; +import { INDEX_NAMES, QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; import { getFilterClause } from '../helper'; -import { INDEX_NAMES, QUERY } from '../../../common/constants'; import { HistogramQueryResult } from './types'; -import { HistogramResult } from '../../../common/types'; +import { HistogramResult } from '../../../../../legacy/plugins/uptime/common/types'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts rename to x-pack/plugins/uptime/server/lib/requests/get_pings.ts index 381aca720dc1d..ddca27d782066 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -5,8 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters/framework'; -import { PingResults, Ping, HttpBody } from '../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../common/constants'; +import { + PingResults, + Ping, + HttpBody, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetPingsParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts rename to x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 8d84c0f4d6769..050e906f01c62 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -5,9 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Snapshot } from '../../../common/runtime_types'; +import { Snapshot } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { + CONTEXT_DEFAULTS, + INDEX_NAMES, +} from '../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './search'; -import { CONTEXT_DEFAULTS, INDEX_NAMES } from '../../../common/constants'; export interface GetSnapshotCountParams { dateRangeStart: string; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/index.ts rename to x-pack/plugins/uptime/server/lib/requests/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts index d519a4e75463f..f542773f32796 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts @@ -12,7 +12,7 @@ import { MonitorGroupsPage, } from '../fetch_page'; import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; const simpleFixture: MonitorGroups[] = [ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index 86506c2c4c044..ea81ec623e01c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -6,7 +6,10 @@ import { QueryContext } from '../query_context'; import { CursorPagination } from '../types'; -import { CursorDirection, SortOrder } from '../../../../../common/graphql/types'; +import { + CursorDirection, + SortOrder, +} from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; describe(QueryContext, () => { // 10 minute range diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts similarity index 88% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts rename to x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index 98b192d14f91a..d96f8dc95aa72 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -5,7 +5,10 @@ */ import { CursorPagination } from '../types'; -import { CursorDirection, SortOrder } from '../../../../../common/graphql/types'; +import { + CursorDirection, + SortOrder, +} from '../../../../../../../legacy/plugins/uptime/common/graphql/types'; import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts rename to x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index e37c749e63566..9ad3928a3b1b2 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -7,14 +7,14 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; import { getHistogramIntervalFormatted } from '../../helper'; -import { INDEX_NAMES, STATES } from '../../../../common/constants'; +import { INDEX_NAMES, STATES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { MonitorSummary, SummaryHistogram, Check, CursorDirection, SortOrder, -} from '../../../../common/graphql/types'; +} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { MonitorEnricher } from './fetch_page'; export const enrichMonitorGroups: MonitorEnricher = async ( diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_chunk.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_chunk.ts rename to x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts rename to x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts index 6440850dc0ffc..62144dacbd377 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts @@ -7,8 +7,12 @@ import { flatten } from 'lodash'; import { CursorPagination } from './types'; import { QueryContext } from './query_context'; -import { QUERY } from '../../../../common/constants'; -import { CursorDirection, MonitorSummary, SortOrder } from '../../../../common/graphql/types'; +import { QUERY } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { + CursorDirection, + MonitorSummary, + SortOrder, +} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; import { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts rename to x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index fc0e35b279e0b..9b3b1186472be 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,8 +5,8 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../../common/constants'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; +import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/index.ts b/x-pack/plugins/uptime/server/lib/requests/search/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/index.ts rename to x-pack/plugins/uptime/server/lib/requests/search/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts rename to x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts index ced557dbf62e0..267551907c5e8 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts @@ -6,7 +6,7 @@ import { QueryContext } from './query_context'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../common/graphql/types'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { MonitorGroups } from './fetch_page'; import { CursorPagination } from './types'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts similarity index 97% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts rename to x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index f5b13c165d87d..c1f5d89ec1a38 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import { APICaller } from 'src/core/server'; -import { INDEX_NAMES } from '../../../../common/constants'; +import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { CursorPagination } from './types'; import { parseRelativeDate } from '../../helper'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts rename to x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index a55301555c8bf..c55aff3e8c4cd 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { INDEX_NAMES } from '../../../../common/constants'; +import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../common/graphql/types'; +import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; /** diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts b/x-pack/plugins/uptime/server/lib/requests/search/types.ts similarity index 76% rename from x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts rename to x-pack/plugins/uptime/server/lib/requests/search/types.ts index dc6021a91146a..42c98ace6e8f5 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/types.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CursorDirection, SortOrder } from '../../../../common/graphql/types'; +import { + CursorDirection, + SortOrder, +} from '../../../../../../legacy/plugins/uptime/common/graphql/types'; export interface CursorPagination { cursorKey?: any; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/types.ts b/x-pack/plugins/uptime/server/lib/requests/types.ts similarity index 89% rename from x-pack/legacy/plugins/uptime/server/lib/requests/types.ts rename to x-pack/plugins/uptime/server/lib/requests/types.ts index e17eb546712a9..53a4e989e3789 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/types.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Ping, PingResults } from '../../../common/graphql/types'; +import { Ping, PingResults } from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { UMElasticsearchQueryFn } from '../adapters'; -import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; +import { + GetPingHistogramParams, + HistogramResult, +} from '../../../../../legacy/plugins/uptime/common/types'; export interface GetAllParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts similarity index 83% rename from x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts rename to x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 73be850306202..8a411368c228f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -5,7 +5,12 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping, MonitorChart, PingResults, StatesIndexStatus } from '../../../common/graphql/types'; +import { + Ping, + MonitorChart, + PingResults, + StatesIndexStatus, +} from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { GetFilterBarParams, GetLatestMonitorParams, @@ -22,10 +27,10 @@ import { MonitorDetails, MonitorLocations, Snapshot, -} from '../../../common/runtime_types'; +} from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; -import { HistogramResult } from '../../../common/types'; +import { HistogramResult } from '../../../../../legacy/plugins/uptime/common/types'; type ESQ = UMElasticsearchQueryFn; diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts new file mode 100644 index 0000000000000..e217b0e2f1ad8 --- /dev/null +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext, CoreStart, CoreSetup } from '../../../../src/core/server'; +import { initServerWithKibana } from './kibana.index'; +import { UptimeCorePlugins } from './lib/adapters'; + +export class Plugin { + constructor(_initializerContext: PluginInitializerContext) {} + public setup(core: CoreSetup, plugins: UptimeCorePlugins) { + initServerWithKibana({ route: core.http.createRouter() }, plugins); + } + public start(_core: CoreStart, _plugins: any) {} +} diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/create_route_with_auth.ts rename to x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/index.ts rename to x-pack/plugins/uptime/server/rest_api/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts rename to x-pack/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/index.ts b/x-pack/plugins/uptime/server/rest_api/index_pattern/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/index.ts rename to x-pack/plugins/uptime/server/rest_api/index_pattern/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts b/x-pack/plugins/uptime/server/rest_api/monitors/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts rename to x-pack/plugins/uptime/server/rest_api/monitors/status.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts rename to x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/index.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/index.ts rename to x-pack/plugins/uptime/server/rest_api/overview_filters/index.ts diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts new file mode 100644 index 0000000000000..21168edfc9744 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts @@ -0,0 +1,48 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +export const createGetAllRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/pings', + validate: { + query: schema.object({ + dateRangeStart: schema.string(), + dateRangeEnd: schema.string(), + location: schema.maybe(schema.string()), + monitorId: schema.maybe(schema.string()), + size: schema.maybe(schema.number()), + sort: schema.maybe(schema.string()), + status: schema.maybe(schema.string()), + }), + }, + options: { + tags: ['access:uptime'], + }, + handler: async ({ callES }, _context, request, response): Promise => { + const { dateRangeStart, dateRangeEnd, location, monitorId, size, sort, status } = request.query; + + const result = await libs.requests.getPings({ + callES, + dateRangeStart, + dateRangeEnd, + monitorId, + status, + sort, + size, + location, + }); + + return response.ok({ + body: { + ...result, + }, + }); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts rename to x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/pings/get_pings.ts rename to x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts b/x-pack/plugins/uptime/server/rest_api/pings/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts rename to x-pack/plugins/uptime/server/rest_api/pings/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts rename to x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts rename to x-pack/plugins/uptime/server/rest_api/snapshot/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/index.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/telemetry/index.ts rename to x-pack/plugins/uptime/server/rest_api/telemetry/index.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts rename to x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts rename to x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/types.ts rename to x-pack/plugins/uptime/server/rest_api/types.ts diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/rest_api/uptime_route_wrapper.ts rename to x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts diff --git a/x-pack/legacy/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/uptime_server.ts rename to x-pack/plugins/uptime/server/uptime_server.ts diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json index 5c3c8cfcab7a6..a0097f53ac93b 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json @@ -1,6 +1,7 @@ [ { "cluster_name": "docker-cluster", + "collectionSource": "monitoring", "cluster_stats": { "indices": { "completion": { @@ -161,6 +162,12 @@ }, "cluster_uuid": "ooEYzl3fSL222Y6eVm7SAA", "license": { + "uid": "79dc3adb-e85e-4cef-a985-9b74eb6c07c1", + "issue_date_in_millis": 1532383643540, + "issued_to": "docker-cluster", + "issuer": "elasticsearch", + "max_nodes": 1000, + "start_date_in_millis": -1, "issue_date": "2018-07-23T22:07:23.540Z", "status": "active", "type": "basic" diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json index 2e6a75ee75972..6cc9c55157b28 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json @@ -1 +1,1001 @@ -[{"cluster_uuid":"6d-9tDFTRe-qT5GoBytdlQ","timestamp":"2017-08-15T22:10:59.952Z","cluster_name":"clustertwo","version":"7.0.0-alpha1","license":{"status":"active","type":"basic","issue_date":"2014-09-29T00:00:00.000Z","expiry_date":"2030-08-29T23:59:59.999Z","expiry_date_in_millis":1914278399999},"cluster_stats":{"timestamp":1502835059952,"status":"green","indices":{"count":1,"shards":{"total":1,"primaries":1,"replication":0,"index":{"shards":{"min":1,"max":1,"avg":1},"primaries":{"min":1,"max":1,"avg":1},"replication":{"min":0,"max":0,"avg":0}}},"docs":{"count":8,"deleted":0},"store":{"size_in_bytes":34095},"fielddata":{"memory_size_in_bytes":0,"evictions":0},"query_cache":{"memory_size_in_bytes":0,"total_count":0,"hit_count":0,"miss_count":0,"cache_size":0,"cache_count":0,"evictions":0},"completion":{"size_in_bytes":0},"segments":{"count":8,"memory_in_bytes":13852,"terms_memory_in_bytes":10412,"stored_fields_memory_in_bytes":2496,"term_vectors_memory_in_bytes":0,"norms_memory_in_bytes":256,"points_memory_in_bytes":8,"doc_values_memory_in_bytes":680,"index_writer_memory_in_bytes":0,"version_map_memory_in_bytes":0,"fixed_bit_set_memory_in_bytes":0,"max_unsafe_auto_id_timestamp":-1,"file_sizes":{}}},"nodes":{"count":{"total":1,"data":1,"coordinating_only":0,"master":1,"ingest":1},"versions":["7.0.0-alpha1"],"os":{"available_processors":4,"allocated_processors":1,"names":[{"name":"Mac OS X","count":1}],"mem":{"total_in_bytes":17179869184,"free_in_bytes":183799808,"used_in_bytes":16996069376,"free_percent":1,"used_percent":99}},"process":{"cpu":{"percent":0},"open_file_descriptors":{"min":163,"max":163,"avg":163}},"jvm":{"max_uptime_in_millis":701043,"versions":[{"vm_version":"25.121-b13","count":1,"vm_vendor":"Oracle Corporation","version":"1.8.0_121","vm_name":"Java HotSpot(TM) 64-Bit Server VM"}],"mem":{"heap_used_in_bytes":204299464,"heap_max_in_bytes":628555776},"threads":40},"fs":{"total_in_bytes":499065712640,"free_in_bytes":200665341952,"available_in_bytes":200403197952},"plugins":[{"classname":"org.elasticsearch.xpack.XPackPlugin","name":"x-pack","description":"Elasticsearch Expanded Pack Plugin","version":"7.0.0-alpha1","has_native_controller":true}],"network_types":{"transport_types":{"security4":1},"http_types":{"security4":1}}}},"stack_stats":{"xpack":{"security":{"available":false,"enabled":true,"realms":{"file":{"available":false,"enabled":false},"ldap":{"available":false,"enabled":false},"native":{"available":false,"enabled":false},"active_directory":{"available":false,"enabled":false},"pki":{"available":false,"enabled":false}},"roles":{"native":{"size":1,"dls":false,"fls":false},"file":{"size":0,"dls":false,"fls":false}},"role_mapping":{"native":{"size":0,"enabled":0}},"ssl":{"http":{"enabled":false}},"audit":{"outputs":["logfile"],"enabled":false},"ipfilter":{"http":false,"transport":false},"anonymous":{"enabled":false}},"monitoring":{"available":true,"enabled":true,"enabled_exporters":{"http":1}},"watcher":{"available":false,"enabled":true,"execution":{"actions":{"_all":{"total":0,"total_time_in_ms":0}}}},"graph":{"available":false,"enabled":true},"ml":{"available":false,"enabled":true,"jobs":{"_all":{"count":0,"detectors":{"total":0,"min":0,"avg":0,"max":0},"model_size":{"total":0,"min":0,"avg":0,"max":0}}},"datafeeds":{"_all":{"count":0}}},"logstash":{"available":false,"enabled":true},"ccr":{"auto_follow_patterns_count":0,"available":true,"follower_indices_count":0,"enabled":true}}}},{"cluster_uuid":"lOF8kofiS_2DX58o9mXJ1Q","timestamp":"2017-08-15T22:10:54.610Z","cluster_name":"monitoring-one","version":"7.0.0-alpha1","license":{"status":"active","type":"trial","issue_date":"2017-08-15T21:58:28.997Z","expiry_date":"2017-09-14T21:58:28.997Z","expiry_date_in_millis":1505426308997},"cluster_stats":{"timestamp":1502835054610,"status":"yellow","indices":{"count":8,"shards":{"total":8,"primaries":8,"replication":0,"index":{"shards":{"min":1,"max":1,"avg":1},"primaries":{"min":1,"max":1,"avg":1},"replication":{"min":0,"max":0,"avg":0}}},"docs":{"count":3997,"deleted":69},"store":{"size_in_bytes":2647163},"fielddata":{"memory_size_in_bytes":2104,"evictions":0},"query_cache":{"memory_size_in_bytes":0,"total_count":0,"hit_count":0,"miss_count":0,"cache_size":0,"cache_count":0,"evictions":0},"completion":{"size_in_bytes":0},"segments":{"count":36,"memory_in_bytes":278961,"terms_memory_in_bytes":166031,"stored_fields_memory_in_bytes":11544,"term_vectors_memory_in_bytes":0,"norms_memory_in_bytes":6784,"points_memory_in_bytes":3250,"doc_values_memory_in_bytes":91352,"index_writer_memory_in_bytes":205347,"version_map_memory_in_bytes":26362,"fixed_bit_set_memory_in_bytes":992,"max_unsafe_auto_id_timestamp":-1,"file_sizes":{}}},"nodes":{"count":{"total":1,"data":1,"coordinating_only":0,"master":1,"ingest":1},"versions":["7.0.0-alpha1"],"os":{"available_processors":4,"allocated_processors":1,"names":[{"name":"Mac OS X","count":1}],"mem":{"total_in_bytes":17179869184,"free_in_bytes":86732800,"used_in_bytes":17093136384,"free_percent":1,"used_percent":99}},"process":{"cpu":{"percent":2},"open_file_descriptors":{"min":178,"max":178,"avg":178}},"jvm":{"max_uptime_in_millis":761002,"versions":[{"vm_version":"25.121-b13","count":1,"vm_vendor":"Oracle Corporation","version":"1.8.0_121","vm_name":"Java HotSpot(TM) 64-Bit Server VM"}],"mem":{"heap_used_in_bytes":133041176,"heap_max_in_bytes":628555776},"threads":42},"fs":{"total_in_bytes":499065712640,"free_in_bytes":200665792512,"available_in_bytes":200403648512},"plugins":[{"classname":"org.elasticsearch.xpack.XPackPlugin","name":"x-pack","description":"Elasticsearch Expanded Pack Plugin","version":"7.0.0-alpha1","has_native_controller":true}],"network_types":{"transport_types":{"security4":1},"http_types":{"security4":1}}}},"stack_stats":{"xpack":{"security":{"available":true,"enabled":true,"realms":{"file":{"name":["default_file"],"available":true,"size":[0],"enabled":true,"order":[2147483647]},"ldap":{"available":true,"enabled":false},"native":{"name":["default_native"],"available":true,"size":[2],"enabled":true,"order":[2147483647]},"active_directory":{"available":true,"enabled":false},"pki":{"available":true,"enabled":false}},"roles":{"native":{"size":1,"dls":false,"fls":false},"file":{"size":0,"dls":false,"fls":false}},"role_mapping":{"native":{"size":0,"enabled":0}},"ssl":{"http":{"enabled":false}},"audit":{"outputs":["logfile"],"enabled":false},"ipfilter":{"http":false,"transport":false},"anonymous":{"enabled":false}},"monitoring":{"available":true,"enabled":true,"enabled_exporters":{"local":1}},"watcher":{"available":true,"enabled":true,"execution":{"actions":{"index":{"total":14,"total_time_in_ms":158},"_all":{"total":110,"total_time_in_ms":2245},"email":{"total":14,"total_time_in_ms":3}}}},"graph":{"available":true,"enabled":true},"ml":{"available":true,"enabled":true,"jobs":{"_all":{"count":0,"detectors":{"total":0,"min":0,"avg":0,"max":0},"model_size":{"total":0,"min":0,"avg":0,"max":0}}},"datafeeds":{"_all":{"count":0}}},"logstash":{"available":true,"enabled":true},"ccr":{"auto_follow_patterns_count":0,"available":true,"follower_indices_count":0,"enabled":true}}}},{"cluster_uuid":"TkHOX_-1TzWwbROwQJU5IA","timestamp":"2017-08-15T22:10:52.642Z","cluster_name":"clusterone","version":"7.0.0-alpha1","license":{"status":"active","type":"trial","issue_date":"2017-08-15T21:58:47.135Z","expiry_date":"2017-09-14T21:58:47.135Z","expiry_date_in_millis":1505426327135},"cluster_stats":{"timestamp":1502835052641,"status":"green","indices":{"count":5,"shards":{"total":26,"primaries":13,"replication":1,"index":{"shards":{"min":2,"max":10,"avg":5.2},"primaries":{"min":1,"max":5,"avg":2.6},"replication":{"min":1,"max":1,"avg":1}}},"docs":{"count":150,"deleted":0},"store":{"size_in_bytes":4838464},"fielddata":{"memory_size_in_bytes":0,"evictions":0},"query_cache":{"memory_size_in_bytes":0,"total_count":0,"hit_count":0,"miss_count":0,"cache_size":0,"cache_count":0,"evictions":0},"completion":{"size_in_bytes":0},"segments":{"count":76,"memory_in_bytes":1907922,"terms_memory_in_bytes":1595112,"stored_fields_memory_in_bytes":23744,"term_vectors_memory_in_bytes":0,"norms_memory_in_bytes":197184,"points_memory_in_bytes":3818,"doc_values_memory_in_bytes":88064,"index_writer_memory_in_bytes":7006184,"version_map_memory_in_bytes":260,"fixed_bit_set_memory_in_bytes":0,"max_unsafe_auto_id_timestamp":1502834982386,"file_sizes":{}}},"nodes":{"count":{"total":2,"data":2,"coordinating_only":0,"master":2,"ingest":2},"versions":["7.0.0-alpha1"],"os":{"available_processors":8,"allocated_processors":2,"names":[{"name":"Mac OS X","count":2}],"mem":{"total_in_bytes":34359738368,"free_in_bytes":332099584,"used_in_bytes":34027638784,"free_percent":1,"used_percent":99}},"process":{"cpu":{"percent":2},"open_file_descriptors":{"min":218,"max":237,"avg":227}},"jvm":{"max_uptime_in_millis":741786,"versions":[{"vm_version":"25.121-b13","count":2,"vm_vendor":"Oracle Corporation","version":"1.8.0_121","vm_name":"Java HotSpot(TM) 64-Bit Server VM"}],"mem":{"heap_used_in_bytes":465621856,"heap_max_in_bytes":1257111552},"threads":92},"fs":{"total_in_bytes":499065712640,"free_in_bytes":200666353664,"available_in_bytes":200404209664},"plugins":[{"classname":"org.elasticsearch.xpack.XPackPlugin","name":"x-pack","description":"Elasticsearch Expanded Pack Plugin","version":"7.0.0-alpha1","has_native_controller":true}],"network_types":{"transport_types":{"security4":2},"http_types":{"security4":2}}}},"stack_stats":{"xpack":{"security":{"available":true,"enabled":true,"realms":{"file":{"name":["default_file"],"available":true,"size":[0],"enabled":true,"order":[2147483647]},"ldap":{"available":true,"enabled":false},"native":{"name":["default_native"],"available":true,"size":[1],"enabled":true,"order":[2147483647]},"active_directory":{"available":true,"enabled":false},"pki":{"available":true,"enabled":false}},"roles":{"native":{"size":1,"dls":false,"fls":false},"file":{"size":0,"dls":false,"fls":false}},"role_mapping":{"native":{"size":0,"enabled":0}},"ssl":{"http":{"enabled":false}},"audit":{"outputs":["logfile"],"enabled":false},"ipfilter":{"http":false,"transport":false},"anonymous":{"enabled":false}},"monitoring":{"available":true,"enabled":true,"enabled_exporters":{"http":1}},"watcher":{"available":true,"enabled":true,"execution":{"actions":{"_all":{"total":0,"total_time_in_ms":0}}}},"graph":{"available":true,"enabled":true,"graph_workspace":{"total":0}},"ml":{"available":true,"enabled":true,"jobs":{"_all":{"count":3,"detectors":{"total":3,"min":1,"avg":1,"max":1},"model_size":{"total":0,"min":0,"avg":0,"max":0}},"opened":{"count":1,"detectors":{"total":1,"min":1,"avg":1,"max":1},"model_size":{"total":0,"min":0,"avg":0,"max":0}},"closed":{"count":2,"detectors":{"total":2,"min":1,"avg":1,"max":1},"model_size":{"total":0,"min":0,"avg":0,"max":0}}},"datafeeds":{"_all":{"count":0}}},"logstash":{"available":true,"enabled":true},"ccr":{"auto_follow_patterns_count":0,"available":true,"follower_indices_count":0,"enabled":true}},"kibana":{"count":1,"versions":[{"version":"7.0.0-alpha1","count":1}],"os":{"platforms":[],"platformReleases":[],"distros":[],"distroReleases":[]},"dashboard":{"total":0},"visualization":{"total":0},"search":{"total":0},"index_pattern":{"total":0},"graph_workspace":{"total":0},"timelion_sheet":{"total":0},"indices":1,"plugins":{}},"logstash":{"count":1,"versions":[{"version":"7.0.0-alpha1","count":1}],"os":{"platforms":[],"platformReleases":[],"distros":[],"distroReleases":[]}}}}] +[ + { + "cluster_uuid": "6d-9tDFTRe-qT5GoBytdlQ", + "collectionSource": "monitoring", + "timestamp": "2017-08-15T22:10:59.952Z", + "cluster_name": "clustertwo", + "version": "7.0.0-alpha1", + "license": { + "status": "active", + "type": "basic", + "issue_date": "2014-09-29T00:00:00.000Z", + "expiry_date": "2030-08-29T23:59:59.999Z", + "expiry_date_in_millis": 1914278399999, + "issue_date_in_millis": 1411948800000, + "issued_to": "issuedTo", + "issuer": "issuer", + "max_nodes": 1, + "uid": "893361dc-9749-4997-93cb-802e3d7fa4a8", + "hkey": null + }, + "cluster_stats": { + "timestamp": 1502835059952, + "status": "green", + "indices": { + "count": 1, + "shards": { + "total": 1, + "primaries": 1, + "replication": 0, + "index": { + "shards": { + "min": 1, + "max": 1, + "avg": 1 + }, + "primaries": { + "min": 1, + "max": 1, + "avg": 1 + }, + "replication": { + "min": 0, + "max": 0, + "avg": 0 + } + } + }, + "docs": { + "count": 8, + "deleted": 0 + }, + "store": { + "size_in_bytes": 34095 + }, + "fielddata": { + "memory_size_in_bytes": 0, + "evictions": 0 + }, + "query_cache": { + "memory_size_in_bytes": 0, + "total_count": 0, + "hit_count": 0, + "miss_count": 0, + "cache_size": 0, + "cache_count": 0, + "evictions": 0 + }, + "completion": { + "size_in_bytes": 0 + }, + "segments": { + "count": 8, + "memory_in_bytes": 13852, + "terms_memory_in_bytes": 10412, + "stored_fields_memory_in_bytes": 2496, + "term_vectors_memory_in_bytes": 0, + "norms_memory_in_bytes": 256, + "points_memory_in_bytes": 8, + "doc_values_memory_in_bytes": 680, + "index_writer_memory_in_bytes": 0, + "version_map_memory_in_bytes": 0, + "fixed_bit_set_memory_in_bytes": 0, + "max_unsafe_auto_id_timestamp": -1, + "file_sizes": {} + } + }, + "nodes": { + "count": { + "total": 1, + "data": 1, + "coordinating_only": 0, + "master": 1, + "ingest": 1 + }, + "versions": [ + "7.0.0-alpha1" + ], + "os": { + "available_processors": 4, + "allocated_processors": 1, + "names": [ + { + "name": "Mac OS X", + "count": 1 + } + ], + "mem": { + "total_in_bytes": 17179869184, + "free_in_bytes": 183799808, + "used_in_bytes": 16996069376, + "free_percent": 1, + "used_percent": 99 + } + }, + "process": { + "cpu": { + "percent": 0 + }, + "open_file_descriptors": { + "min": 163, + "max": 163, + "avg": 163 + } + }, + "jvm": { + "max_uptime_in_millis": 701043, + "versions": [ + { + "vm_version": "25.121-b13", + "count": 1, + "vm_vendor": "Oracle Corporation", + "version": "1.8.0_121", + "vm_name": "Java HotSpot(TM) 64-Bit Server VM" + } + ], + "mem": { + "heap_used_in_bytes": 204299464, + "heap_max_in_bytes": 628555776 + }, + "threads": 40 + }, + "fs": { + "total_in_bytes": 499065712640, + "free_in_bytes": 200665341952, + "available_in_bytes": 200403197952 + }, + "plugins": [ + { + "classname": "org.elasticsearch.xpack.XPackPlugin", + "name": "x-pack", + "description": "Elasticsearch Expanded Pack Plugin", + "version": "7.0.0-alpha1", + "has_native_controller": true + } + ], + "network_types": { + "transport_types": { + "security4": 1 + }, + "http_types": { + "security4": 1 + } + } + } + }, + "stack_stats": { + "xpack": { + "security": { + "available": false, + "enabled": true, + "realms": { + "file": { + "available": false, + "enabled": false + }, + "ldap": { + "available": false, + "enabled": false + }, + "native": { + "available": false, + "enabled": false + }, + "active_directory": { + "available": false, + "enabled": false + }, + "pki": { + "available": false, + "enabled": false + } + }, + "roles": { + "native": { + "size": 1, + "dls": false, + "fls": false + }, + "file": { + "size": 0, + "dls": false, + "fls": false + } + }, + "role_mapping": { + "native": { + "size": 0, + "enabled": 0 + } + }, + "ssl": { + "http": { + "enabled": false + } + }, + "audit": { + "outputs": [ + "logfile" + ], + "enabled": false + }, + "ipfilter": { + "http": false, + "transport": false + }, + "anonymous": { + "enabled": false + } + }, + "monitoring": { + "available": true, + "enabled": true, + "enabled_exporters": { + "http": 1 + } + }, + "watcher": { + "available": false, + "enabled": true, + "execution": { + "actions": { + "_all": { + "total": 0, + "total_time_in_ms": 0 + } + } + } + }, + "graph": { + "available": false, + "enabled": true + }, + "ml": { + "available": false, + "enabled": true, + "jobs": { + "_all": { + "count": 0, + "detectors": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + } + }, + "datafeeds": { + "_all": { + "count": 0 + } + } + }, + "logstash": { + "available": false, + "enabled": true + }, + "ccr": { + "auto_follow_patterns_count": 0, + "available": true, + "follower_indices_count": 0, + "enabled": true + } + } + } + }, + { + "cluster_uuid": "lOF8kofiS_2DX58o9mXJ1Q", + "collectionSource": "monitoring", + "timestamp": "2017-08-15T22:10:54.610Z", + "cluster_name": "monitoring-one", + "version": "7.0.0-alpha1", + "license": { + "status": "active", + "type": "trial", + "issue_date": "2017-08-15T21:58:28.997Z", + "expiry_date": "2017-09-14T21:58:28.997Z", + "expiry_date_in_millis": 1505426308997, + "issue_date_in_millis": 1502834308997, + "issued_to": "monitoring-one", + "issuer": "elasticsearch", + "max_nodes": 1000, + "start_date_in_millis": -1, + "uid": "e5f6e897-0db5-4042-ad7c-6628ddc91691", + "hkey": null + }, + "cluster_stats": { + "timestamp": 1502835054610, + "status": "yellow", + "indices": { + "count": 8, + "shards": { + "total": 8, + "primaries": 8, + "replication": 0, + "index": { + "shards": { + "min": 1, + "max": 1, + "avg": 1 + }, + "primaries": { + "min": 1, + "max": 1, + "avg": 1 + }, + "replication": { + "min": 0, + "max": 0, + "avg": 0 + } + } + }, + "docs": { + "count": 3997, + "deleted": 69 + }, + "store": { + "size_in_bytes": 2647163 + }, + "fielddata": { + "memory_size_in_bytes": 2104, + "evictions": 0 + }, + "query_cache": { + "memory_size_in_bytes": 0, + "total_count": 0, + "hit_count": 0, + "miss_count": 0, + "cache_size": 0, + "cache_count": 0, + "evictions": 0 + }, + "completion": { + "size_in_bytes": 0 + }, + "segments": { + "count": 36, + "memory_in_bytes": 278961, + "terms_memory_in_bytes": 166031, + "stored_fields_memory_in_bytes": 11544, + "term_vectors_memory_in_bytes": 0, + "norms_memory_in_bytes": 6784, + "points_memory_in_bytes": 3250, + "doc_values_memory_in_bytes": 91352, + "index_writer_memory_in_bytes": 205347, + "version_map_memory_in_bytes": 26362, + "fixed_bit_set_memory_in_bytes": 992, + "max_unsafe_auto_id_timestamp": -1, + "file_sizes": {} + } + }, + "nodes": { + "count": { + "total": 1, + "data": 1, + "coordinating_only": 0, + "master": 1, + "ingest": 1 + }, + "versions": [ + "7.0.0-alpha1" + ], + "os": { + "available_processors": 4, + "allocated_processors": 1, + "names": [ + { + "name": "Mac OS X", + "count": 1 + } + ], + "mem": { + "total_in_bytes": 17179869184, + "free_in_bytes": 86732800, + "used_in_bytes": 17093136384, + "free_percent": 1, + "used_percent": 99 + } + }, + "process": { + "cpu": { + "percent": 2 + }, + "open_file_descriptors": { + "min": 178, + "max": 178, + "avg": 178 + } + }, + "jvm": { + "max_uptime_in_millis": 761002, + "versions": [ + { + "vm_version": "25.121-b13", + "count": 1, + "vm_vendor": "Oracle Corporation", + "version": "1.8.0_121", + "vm_name": "Java HotSpot(TM) 64-Bit Server VM" + } + ], + "mem": { + "heap_used_in_bytes": 133041176, + "heap_max_in_bytes": 628555776 + }, + "threads": 42 + }, + "fs": { + "total_in_bytes": 499065712640, + "free_in_bytes": 200665792512, + "available_in_bytes": 200403648512 + }, + "plugins": [ + { + "classname": "org.elasticsearch.xpack.XPackPlugin", + "name": "x-pack", + "description": "Elasticsearch Expanded Pack Plugin", + "version": "7.0.0-alpha1", + "has_native_controller": true + } + ], + "network_types": { + "transport_types": { + "security4": 1 + }, + "http_types": { + "security4": 1 + } + } + } + }, + "stack_stats": { + "xpack": { + "security": { + "available": true, + "enabled": true, + "realms": { + "file": { + "name": [ + "default_file" + ], + "available": true, + "size": [ + 0 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "ldap": { + "available": true, + "enabled": false + }, + "native": { + "name": [ + "default_native" + ], + "available": true, + "size": [ + 2 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "active_directory": { + "available": true, + "enabled": false + }, + "pki": { + "available": true, + "enabled": false + } + }, + "roles": { + "native": { + "size": 1, + "dls": false, + "fls": false + }, + "file": { + "size": 0, + "dls": false, + "fls": false + } + }, + "role_mapping": { + "native": { + "size": 0, + "enabled": 0 + } + }, + "ssl": { + "http": { + "enabled": false + } + }, + "audit": { + "outputs": [ + "logfile" + ], + "enabled": false + }, + "ipfilter": { + "http": false, + "transport": false + }, + "anonymous": { + "enabled": false + } + }, + "monitoring": { + "available": true, + "enabled": true, + "enabled_exporters": { + "local": 1 + } + }, + "watcher": { + "available": true, + "enabled": true, + "execution": { + "actions": { + "index": { + "total": 14, + "total_time_in_ms": 158 + }, + "_all": { + "total": 110, + "total_time_in_ms": 2245 + }, + "email": { + "total": 14, + "total_time_in_ms": 3 + } + } + } + }, + "graph": { + "available": true, + "enabled": true + }, + "ml": { + "available": true, + "enabled": true, + "jobs": { + "_all": { + "count": 0, + "detectors": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + } + }, + "datafeeds": { + "_all": { + "count": 0 + } + } + }, + "logstash": { + "available": true, + "enabled": true + }, + "ccr": { + "auto_follow_patterns_count": 0, + "available": true, + "follower_indices_count": 0, + "enabled": true + } + } + } + }, + { + "cluster_uuid": "TkHOX_-1TzWwbROwQJU5IA", + "collectionSource": "monitoring", + "timestamp": "2017-08-15T22:10:52.642Z", + "cluster_name": "clusterone", + "version": "7.0.0-alpha1", + "license": { + "status": "active", + "type": "trial", + "issue_date": "2017-08-15T21:58:47.135Z", + "expiry_date": "2017-09-14T21:58:47.135Z", + "expiry_date_in_millis": 1505426327135, + "issue_date_in_millis": 1502834327135, + "issued_to": "clusterone", + "issuer": "elasticsearch", + "max_nodes": 1000, + "start_date_in_millis": -1, + "uid": "e5e99511-0928-41a3-97b0-ec77fa5e3b4c", + "hkey": null + }, + "cluster_stats": { + "timestamp": 1502835052641, + "status": "green", + "indices": { + "count": 5, + "shards": { + "total": 26, + "primaries": 13, + "replication": 1, + "index": { + "shards": { + "min": 2, + "max": 10, + "avg": 5.2 + }, + "primaries": { + "min": 1, + "max": 5, + "avg": 2.6 + }, + "replication": { + "min": 1, + "max": 1, + "avg": 1 + } + } + }, + "docs": { + "count": 150, + "deleted": 0 + }, + "store": { + "size_in_bytes": 4838464 + }, + "fielddata": { + "memory_size_in_bytes": 0, + "evictions": 0 + }, + "query_cache": { + "memory_size_in_bytes": 0, + "total_count": 0, + "hit_count": 0, + "miss_count": 0, + "cache_size": 0, + "cache_count": 0, + "evictions": 0 + }, + "completion": { + "size_in_bytes": 0 + }, + "segments": { + "count": 76, + "memory_in_bytes": 1907922, + "terms_memory_in_bytes": 1595112, + "stored_fields_memory_in_bytes": 23744, + "term_vectors_memory_in_bytes": 0, + "norms_memory_in_bytes": 197184, + "points_memory_in_bytes": 3818, + "doc_values_memory_in_bytes": 88064, + "index_writer_memory_in_bytes": 7006184, + "version_map_memory_in_bytes": 260, + "fixed_bit_set_memory_in_bytes": 0, + "max_unsafe_auto_id_timestamp": 1502834982386, + "file_sizes": {} + } + }, + "nodes": { + "count": { + "total": 2, + "data": 2, + "coordinating_only": 0, + "master": 2, + "ingest": 2 + }, + "versions": [ + "7.0.0-alpha1" + ], + "os": { + "available_processors": 8, + "allocated_processors": 2, + "names": [ + { + "name": "Mac OS X", + "count": 2 + } + ], + "mem": { + "total_in_bytes": 34359738368, + "free_in_bytes": 332099584, + "used_in_bytes": 34027638784, + "free_percent": 1, + "used_percent": 99 + } + }, + "process": { + "cpu": { + "percent": 2 + }, + "open_file_descriptors": { + "min": 218, + "max": 237, + "avg": 227 + } + }, + "jvm": { + "max_uptime_in_millis": 741786, + "versions": [ + { + "vm_version": "25.121-b13", + "count": 2, + "vm_vendor": "Oracle Corporation", + "version": "1.8.0_121", + "vm_name": "Java HotSpot(TM) 64-Bit Server VM" + } + ], + "mem": { + "heap_used_in_bytes": 465621856, + "heap_max_in_bytes": 1257111552 + }, + "threads": 92 + }, + "fs": { + "total_in_bytes": 499065712640, + "free_in_bytes": 200666353664, + "available_in_bytes": 200404209664 + }, + "plugins": [ + { + "classname": "org.elasticsearch.xpack.XPackPlugin", + "name": "x-pack", + "description": "Elasticsearch Expanded Pack Plugin", + "version": "7.0.0-alpha1", + "has_native_controller": true + } + ], + "network_types": { + "transport_types": { + "security4": 2 + }, + "http_types": { + "security4": 2 + } + } + } + }, + "stack_stats": { + "xpack": { + "security": { + "available": true, + "enabled": true, + "realms": { + "file": { + "name": [ + "default_file" + ], + "available": true, + "size": [ + 0 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "ldap": { + "available": true, + "enabled": false + }, + "native": { + "name": [ + "default_native" + ], + "available": true, + "size": [ + 1 + ], + "enabled": true, + "order": [ + 2147483647 + ] + }, + "active_directory": { + "available": true, + "enabled": false + }, + "pki": { + "available": true, + "enabled": false + } + }, + "roles": { + "native": { + "size": 1, + "dls": false, + "fls": false + }, + "file": { + "size": 0, + "dls": false, + "fls": false + } + }, + "role_mapping": { + "native": { + "size": 0, + "enabled": 0 + } + }, + "ssl": { + "http": { + "enabled": false + } + }, + "audit": { + "outputs": [ + "logfile" + ], + "enabled": false + }, + "ipfilter": { + "http": false, + "transport": false + }, + "anonymous": { + "enabled": false + } + }, + "monitoring": { + "available": true, + "enabled": true, + "enabled_exporters": { + "http": 1 + } + }, + "watcher": { + "available": true, + "enabled": true, + "execution": { + "actions": { + "_all": { + "total": 0, + "total_time_in_ms": 0 + } + } + } + }, + "graph": { + "available": true, + "enabled": true, + "graph_workspace": { + "total": 0 + } + }, + "ml": { + "available": true, + "enabled": true, + "jobs": { + "_all": { + "count": 3, + "detectors": { + "total": 3, + "min": 1, + "avg": 1, + "max": 1 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + }, + "opened": { + "count": 1, + "detectors": { + "total": 1, + "min": 1, + "avg": 1, + "max": 1 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + }, + "closed": { + "count": 2, + "detectors": { + "total": 2, + "min": 1, + "avg": 1, + "max": 1 + }, + "model_size": { + "total": 0, + "min": 0, + "avg": 0, + "max": 0 + } + } + }, + "datafeeds": { + "_all": { + "count": 0 + } + } + }, + "logstash": { + "available": true, + "enabled": true + }, + "ccr": { + "auto_follow_patterns_count": 0, + "available": true, + "follower_indices_count": 0, + "enabled": true + } + }, + "kibana": { + "count": 1, + "versions": [ + { + "version": "7.0.0-alpha1", + "count": 1 + } + ], + "os": { + "platforms": [], + "platformReleases": [], + "distros": [], + "distroReleases": [] + }, + "dashboard": { + "total": 0 + }, + "visualization": { + "total": 0 + }, + "search": { + "total": 0 + }, + "index_pattern": { + "total": 0 + }, + "graph_workspace": { + "total": 0 + }, + "timelion_sheet": { + "total": 0 + }, + "indices": 1, + "plugins": {} + }, + "logstash": { + "count": 1, + "versions": [ + { + "version": "7.0.0-alpha1", + "count": 1 + } + ], + "os": { + "platforms": [], + "platformReleases": [], + "distros": [], + "distroReleases": [] + } + } + } + } +] diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts index f89905f0da04f..4d3167b14b86f 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts @@ -13,7 +13,8 @@ export const makePing = async ( es: any, monitorId: string, fields: { [key: string]: any }, - mogrify: (doc: any) => any + mogrify: (doc: any) => any, + refresh: boolean = true ) => { const baseDoc = { tcp: { @@ -103,7 +104,7 @@ export const makePing = async ( await es.index({ index: INDEX_NAME, - refresh: true, + refresh, body: doc, }); @@ -115,7 +116,8 @@ export const makeCheck = async ( monitorId: string, numIps: number, fields: { [key: string]: any }, - mogrify: (doc: any) => any + mogrify: (doc: any) => any, + refresh: boolean = true ) => { const cgFields = { monitor: { @@ -137,11 +139,16 @@ export const makeCheck = async ( if (i === numIps - 1) { pingFields.summary = summary; } - const doc = await makePing(es, monitorId, pingFields, mogrify); + const doc = await makePing(es, monitorId, pingFields, mogrify, false); docs.push(doc); // @ts-ignore summary[doc.monitor.status]++; } + + if (refresh) { + es.indices.refresh(); + } + return docs; }; @@ -152,7 +159,8 @@ export const makeChecks = async ( numIps: number, every: number, // number of millis between checks fields: { [key: string]: any } = {}, - mogrify: (doc: any) => any = d => d + mogrify: (doc: any) => any = d => d, + refresh: boolean = true ) => { const checks = []; const oldestTime = new Date().getTime() - numChecks * every; @@ -169,7 +177,11 @@ export const makeChecks = async ( }, }, }); - checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify)); + checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify, false)); + } + + if (refresh) { + es.indices.refresh(); } return checks; @@ -183,19 +195,29 @@ export const makeChecksWithStatus = async ( every: number, fields: { [key: string]: any } = {}, status: 'up' | 'down', - mogrify: (doc: any) => any = d => d + mogrify: (doc: any) => any = d => d, + refresh: boolean = true ) => { const oppositeStatus = status === 'up' ? 'down' : 'up'; - return await makeChecks(es, monitorId, numChecks, numIps, every, fields, d => { - d.monitor.status = status; - if (d.summary) { - d.summary[status] += d.summary[oppositeStatus]; - d.summary[oppositeStatus] = 0; - } - - return mogrify(d); - }); + return await makeChecks( + es, + monitorId, + numChecks, + numIps, + every, + fields, + d => { + d.monitor.status = status; + if (d.summary) { + d.summary[status] += d.summary[oppositeStatus]; + d.summary[oppositeStatus] = 0; + } + + return mogrify(d); + }, + refresh + ); }; // Helper for processing a list of checks to find the time picker bounds. diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts index 429f50ec0aa5b..0982d5fef7cb4 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_histogram.ts @@ -6,7 +6,7 @@ import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { assertCloseTo } from '../../../../../legacy/plugins/uptime/server/lib/helper'; +import { assertCloseTo } from '../../../../../plugins/uptime/server/lib/helper'; export default function({ getService }: FtrProviderContext) { describe('pingHistogram', () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index 79a1e667e5458..a1cb60483c332 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -74,6 +74,17 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should report that it failed to import a thousand and one (10001) simple rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(new Array(10001).fill('rule-1')), 'rules.ndjson') + .query() + .expect(500); + + expect(body).to.eql({ message: "Can't import more than 10000 rules", status_code: 500 }); + }); + it('should be able to read an imported rule back out correctly', async () => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 87ae5231d1031..1dd069bb907d1 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -28,8 +28,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY: https://github.com/elastic/kibana/issues/45348 - describe.skip('security', () => { + describe('security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 1dfbe3526ce40..ecad5a40ec42e 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -15,8 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraLogs']); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/58059 - describe.skip('Logs Source Configuration', function() { + describe('Logs Source Configuration', function() { this.tags('smoke'); before(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts index 798a04cae3740..1bcdeef394c00 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts @@ -36,7 +36,7 @@ export default function({ getService }: FtrProviderContext) { }, dependentVariable: 'y', trainingPercent: '20', - modelMemory: '105mb', + modelMemory: '200mb', createIndexPattern: true, expected: { row: { diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz index b38981c03417e..58ac5616651d4 100644 Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz index c2050b3399ab7..2b204d0bde271 100644 Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/nanos/data.json.gz b/x-pack/test/functional/es_archives/reporting/nanos/data.json.gz index d0531c7607736..2811c495aae2d 100644 Binary files a/x-pack/test/functional/es_archives/reporting/nanos/data.json.gz and b/x-pack/test/functional/es_archives/reporting/nanos/data.json.gz differ diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 3bcba7cbd1696..5889a374e443e 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -30,6 +30,11 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const rawDataTabLocator = 'a[id=rawdata-tab]'; await PageObjects.common.navigateToApp('login'); + + // ensure welcome screen won't be shown. This is relevant for environments which don't allow + // to use the yml setting, e.g. cloud + await browser.setLocalStorageItem('home:welcome:show', 'false'); + await testSubjects.setValue('loginUsername', username); await testSubjects.setValue('loginPassword', password); await testSubjects.click('loginSubmit'); diff --git a/x-pack/test/functional/services/infra_source_configuration_form.ts b/x-pack/test/functional/services/infra_source_configuration_form.ts index ab61d5232fa1c..dbae6f00f75a2 100644 --- a/x-pack/test/functional/services/infra_source_configuration_form.ts +++ b/x-pack/test/functional/services/infra_source_configuration_form.ts @@ -36,25 +36,37 @@ export function InfraSourceConfigurationFormProvider({ getService }: FtrProvider return await testSubjects.find('~addLogColumnPopover'); }, async addTimestampLogColumn() { - await (await this.getAddLogColumnButton()).click(); + // try to open the popover + const popover = await retry.try(async () => { + await (await this.getAddLogColumnButton()).click(); + return this.getAddLogColumnPopover(); + }); + + // try to select the timestamp field await retry.try(async () => { - await ( - await testSubjects.findDescendant( - '~addTimestampLogColumn', - await this.getAddLogColumnPopover() - ) - ).click(); + await (await testSubjects.findDescendant('~addTimestampLogColumn', popover)).click(); }); + + // wait for timestamp panel to show up + await testSubjects.findDescendant('~systemLogColumnPanel:Timestamp', await this.getForm()); }, async addFieldLogColumn(fieldName: string) { - await (await this.getAddLogColumnButton()).click(); + // try to open the popover + const popover = await retry.try(async () => { + await (await this.getAddLogColumnButton()).click(); + return this.getAddLogColumnPopover(); + }); + + // try to select the given field await retry.try(async () => { - const popover = await this.getAddLogColumnPopover(); await (await testSubjects.findDescendant('~fieldSearchInput', popover)).type(fieldName); await ( await testSubjects.findDescendant(`~addFieldLogColumn:${fieldName}`, popover) ).click(); }); + + // wait for field panel to show up + await testSubjects.findDescendant(`~fieldLogColumnPanel:${fieldName}`, await this.getForm()); }, async getLogColumnPanels(): Promise { return await testSubjects.findAllDescendant('~logColumnPanel', await this.getForm()); diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts index b4e455ebaa63f..96dc8993c3d35 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts @@ -6,10 +6,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommon } from './common'; -export function MachineLearningDataFrameAnalyticsCreationProvider({ - getService, -}: FtrProviderContext) { +export function MachineLearningDataFrameAnalyticsCreationProvider( + { getService }: FtrProviderContext, + mlCommon: MlCommon +) { const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); const retry = getService('retry'); @@ -85,14 +87,14 @@ export function MachineLearningDataFrameAnalyticsCreationProvider({ }, async setJobId(jobId: string) { - await testSubjects.setValue('mlAnalyticsCreateJobFlyoutJobIdInput', jobId, { + await mlCommon.setValueWithChecks('mlAnalyticsCreateJobFlyoutJobIdInput', jobId, { clearWithKeyboard: true, }); await this.assertJobIdValue(jobId); }, async setJobDescription(jobDescription: string) { - await testSubjects.setValue('mlDFAnalyticsJobCreationJobDescription', jobDescription, { + await mlCommon.setValueWithChecks('mlDFAnalyticsJobCreationJobDescription', jobDescription, { clearWithKeyboard: true, }); await this.assertJobDescriptionValue(jobDescription); @@ -136,9 +138,13 @@ export function MachineLearningDataFrameAnalyticsCreationProvider({ }, async setDestIndex(destIndex: string) { - await testSubjects.setValue('mlAnalyticsCreateJobFlyoutDestinationIndexInput', destIndex, { - clearWithKeyboard: true, - }); + await mlCommon.setValueWithChecks( + 'mlAnalyticsCreateJobFlyoutDestinationIndexInput', + destIndex, + { + clearWithKeyboard: true, + } + ); await this.assertDestIndexValue(destIndex); }, @@ -248,7 +254,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider({ }, async setModelMemory(modelMemory: string) { - await testSubjects.setValue('mlAnalyticsCreateJobFlyoutModelMemoryInput', modelMemory, { + await mlCommon.setValueWithChecks('mlAnalyticsCreateJobFlyoutModelMemoryInput', modelMemory, { clearWithKeyboard: true, }); await this.assertModelMemoryValue(modelMemory); diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index 2660a90662dec..354e0907375ca 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -42,7 +42,10 @@ export function MachineLearningProvider(context: FtrProviderContext) { const api = MachineLearningAPIProvider(context); const customUrls = MachineLearningCustomUrlsProvider(context); const dataFrameAnalytics = MachineLearningDataFrameAnalyticsProvider(context, api); - const dataFrameAnalyticsCreation = MachineLearningDataFrameAnalyticsCreationProvider(context); + const dataFrameAnalyticsCreation = MachineLearningDataFrameAnalyticsCreationProvider( + context, + common + ); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); const dataVisualizer = MachineLearningDataVisualizerProvider(context); const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider(context); diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js index 9fc8078162847..38fc1f0c6356f 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js @@ -6,10 +6,7 @@ import expect from '@kbn/expect'; -import { - ReindexStatus, - REINDEX_OP_TYPE, -} from '../../../legacy/plugins/upgrade_assistant/common/types'; +import { ReindexStatus, REINDEX_OP_TYPE } from '../../../plugins/upgrade_assistant/common/types'; export default function({ getService }) { const supertest = getService('supertest'); diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 978271166cc05..723da7cef6a77 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -36,7 +36,7 @@ "x-pack/test_utils/*" ], "plugins/*": ["src/legacy/core_plugins/*/public/"], - "fixtures/*": ["src/fixtures/*"] + "fixtures/*": ["src/fixtures/*"], }, "types": [ "node", diff --git a/yarn.lock b/yarn.lock index f46e869909e2e..7906f363813b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5684,6 +5684,13 @@ text-table "^0.2.0" webpack-log "^1.1.2" +"@welldone-software/why-did-you-render@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-4.0.0.tgz#cc98c996f5a06ea55bd07dc99ba4b4d68af93332" + integrity sha512-PjqriZ8Ak9biP2+kOcIrg+NwsFwWVhGV03Hm+ns84YBCArn+hWBKM9rMBEU6e62I1qyrYF2/G9yktNpEmfWfJA== + dependencies: + lodash "^4" + "@wry/context@^0.4.0": version "0.4.1" resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.1.tgz#b3e23ca036035cbad0bd9711269352dd03a6fe3c" @@ -19767,7 +19774,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==